XmlSerializerクラス

XML形式でシリアライズ

BinaryFormatterクラスの項ではオブジェクトをシリアライズする方法を説明しました。
BinaryFormatterはオブジェクトをbyte型配列(バイナリ)に変換しますが、XmlSerializerクラスを使用するとXML形式に変換することができます。

バイナリファイルの値を直接編集するのは困難ですが、XMLはテキストデータですので普通のテキストエディタで編集することができます。
例えばアプリケーションの設定をXMLファイルで保存しておけば、外部から設定を簡単に変更できるようになります。

XmlSerializerクラスは「System.Xml.Serialization」名前空間にあるので、コード先頭のusingに追加しておきます。
Streamクラスも使用するので、「System.IO」も追加しておきます。


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
//↓を追加
using System.Xml.Serialization;
using System.IO;

シリアライズ

BinaryFormatterクラスの場合はシリアライズしたいクラスの先頭にSerializableAttributeを付ける必要がありましたが、XmlSerializerクラスの場合はその必要はありません。
その代わり、以下のような制限があります。

  • クラスがpublicな場所にあること
  • クラス自体もpublicであること
  • シリアライズするフィールド、プロパティはpublicであること
  • デフォルトコンストラクターがあること
    (明示的でなくてもよい)

プロジェクト作成時にVisual Studioが自動的に生成するコードに存在する「Program」クラスはアクセス修飾子が付いていないためprivateなクラスです。
このProgramクラスの内部にクラスを定義すると、そのクラスは外部(Programクラス以外)からは見えません。
こういったクラスはXmlSerializerではシリアライズできません。
そのため、Programクラスの外部にpublicなクラスを定義するか、Programクラスをpublicに変更します。

ここではProgramクラスをpublicに変更し、その内部の自作クラスをシリアライズしてみます。


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Xml.Serialization;

namespace ConsoleApplication1
{
    public class Program
    {
        public class SimpleClass
        {
            public int Num;
            public string Str;

            public SimpleClass() { }

            public SimpleClass(int n, string s)
            {
                Num = n;
                Str = s;
            }
        }

        static void Main(string[] args)
        {
        }
    }
}

XmlSerializerはシリアル化したデータの書き込み先にストリームを利用します。
ここでは外部ファイルに保存するためにStreamWriterクラスを使用します。


string path = "test.xml";

SimpleClass sc = new SimpleClass(123, "abc");
using (StreamWriter sw = new StreamWriter(path))
{
    //SimpleClass用の
    //XmlSerializerを生成
    XmlSerializer xs = new XmlSerializer(typeof(SimpleClass));
    
    //データをシリアル化して
    //StreamWriterに書き込み
    xs.Serialize(sw, sc);
}

データをシリアル化するにはまずXmlSerializerのインスタンスを生成します。
コンストクターにはシリアル化したいクラスのTypeオブジェクトを指定します。
サンプルコードのようにtypeof演算子に目的のクラス型を指定することでTypeオブジェクトが得られます。

実際にシリアライズするにはSerializeメソッドを使用します。
第一引数はシリアル化したデータを書き込むStreamを指定します。
第二引数はシリアル化したいデータを指定します。

これで指定のパスにSimpleClassをシリアライズしたXMLファイルが作成されます。
テキストエディタで開くと以下のようなXMLが生成されていることが確認できます。

<?xml version="1.0" encoding="utf-8"?>
<SimpleClass xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Num>123</Num>
  <Str>abc</Str>
</SimpleClass>

このファイルの「123」や「abc」の箇所がSimpleClassのフィールドの値です。
これを書き換えるとデシリアイズ時にその値が反映されます。
フィールドの値以外の箇所を書き換えるとデータが読み込めなくなる恐れがあるので注意してください。

デシリアライズ

デシリアライズにはDeserializeメソッドを使用します。


string path = "test.xml";

SimpleClass sc;
using (StreamReader sr = new StreamReader(path))
{
    XmlSerializer xs = new XmlSerializer(typeof(SimpleClass));
    sc = (SimpleClass)xs.Deserialize(sr);
}

Console.WriteLine("{0} {1}", sc.Num, sc.Str);
123 abc

引数には復元したい対象のStreamを指定します。
今回は先ほどシリアライズしたデータをStreamReaderで読み込み、それを渡しています。
戻り値はobject型なので、適切な型にキャストします。

これでインスタンスscにはファイルから読み取った値が復元されます。

シリアライズから除外するフィールドの指定

標準ではクラスのpublicなフィールド(と自動実装プロパティ)の状態がシリアライズされますが、シリアライズの対象から外したいフィールドがある場合はそのフィールドにXmlIgnoreAttributeを付加します。


class SimpleClass
{
    [XmlIgnore]
    public int Num;
    public string Str;

    public SimpleClass() { }

    public SimpleClass(int n, string s)
    {
        Num = n;
        Str = s;
    }
}

string path = "test.bin";

SimpleClass sc = new SimpleClass(123, "abc");
using (StreamWriter sw = new StreamWriter(path))
{
    XmlSerializer xs = new XmlSerializer(typeof(SimpleClass));
    xs.Serialize(fs, sw);
}

using (StreamWriter sw = new StreamWriter(path))
{
    XmlSerializer xs = new XmlSerializer(typeof(SimpleClass));
    sc = (SimpleClass)xs.Deserialize(sw);
}

Console.WriteLine("{0} {1}", sc.Num, sc.Str);
0 abc

XmlIgnoreAttribute属性が付けられたフィールドNumの値はファイルに保存されず、復元しても規定値である「0」が割り当てられていることが分かります。
(正確に言えばデフォルトコンストラクター内で割り当てられた値となります)
XmlIgnoreAttribute属性はフィールドの直前に付ける必要があるので、フィールドNumのすぐ下のフィールドStrには影響しません。

フィールドの追加

BinaryFormatterでは後からクラスにフィールドを追加すると、過去に保存したデータを読み込むにはOptionalFieldAttributeを使用する必要がありました。
XmlSerializerではそういった必要はなく、クラスのデフォルトコンストラクターを呼び出した後にXMLファイルからフィールドに値を割り当てます。
つまりXMLファイルにデータが存在しないフィールドにはコンストラクターで初期化した値が割り当てられます。
(デフォルトコンストラクター内で何もしない場合は規定値が割り当てられます)

反対に、後からフィールドを削除した場合でも問題なく読み込むことができます。

XMLの要素名のカスタマイズ

XMLの要素名(タグ名)は標準ではクラス名やフィールド名がそのまま使用されます。
アプリケーションの設定にXMLファイルを使用する場合は以下の属性を付けることで要素名をわかりやすいものに変えることができます。

属性名 説明
XmlRootAttribute クラスや構造体のルート要素名
XmlTypeAttribute クラスや構造体のルート要素を含む親要素名
XmlElementAttribute フィールドやプロパティの要素名

[XmlType("生徒情報")]
public class Student
{
    [XmlElement("生徒番号")]
    public int Number;

    [XmlElement("名前")]
    public string Name;

    public Student() { }

    public Student(int number, string name)
    {
        Number = number;
        Name = name;
    }

    public Student(Student student)
    {
        Number = student.Number;
        Name = student.Name;
    }
}

[XmlRoot("学籍簿")]
public class SchoolClass
{
    [XmlElement("学級番号")]
    public int ClassNumber;

    public Student[] Students;

    public SchoolClass() { }

    public SchoolClass(int classNumber, Student[] students)
    {
        ClassNumber = classNumber;
        Students = new Student[students.Length];
        for (int i = 0; i < students.Length; i++)
            Students[i] = new Student(students[i]);
    }
}

string path = "test.xml";

SchoolClass schoolClass = new SchoolClass(1, new Student[] {
    new Student(1, "A山B太"),
    new Student(2, "C谷D男")
});

using (StreamWriter sw = new StreamWriter(path))
{
    XmlSerializer xs = new XmlSerializer(typeof(SchoolClass));
    xs.Serialize(sw, schoolClass);
}

XMLは以下のようになります。

<?xml version="1.0" encoding="utf-8"?>
<学籍簿 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <学級番号>1</学級番号>
  <Students>
    <生徒情報>
      <生徒番号>1</生徒番号>
      <名前>A山B太</名前>
    </生徒情報>
    <生徒情報>
      <生徒番号>2</生徒番号>
      <名前>C谷D男</名前>
    </生徒情報>
  </Students>
</学籍簿>

XmlRootAttributeはクラスがルート要素になる場合にのみ有効です。
XmlTypeAttributeはルート要素と、それ以外の親要素になる場合にも有効です。
XmlElementAttributeはフィールドやプロパティに使用できます。

配列、Listの要素名

配列やListをそのままXMLにシリアライズすると以下のような形式になります。


public class SimpleClass
{
    public int[] array = 
        new int[] { 123, 456, 789 };
}

string path = "test.xml";

SimpleClass sc = new SimpleClass();
using (StreamWriter sw = new StreamWriter(path))
{
    XmlSerializer xs = new XmlSerializer(typeof(SimpleClass));
    xs.Serialize(sw, sc);
}
<?xml version="1.0" encoding="utf-8"?>
<SimpleClass xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <array>
    <int>123</int>
    <int>456</int>
    <int>789</int>
  </array>
</SimpleClass>

配列の要素名や値の要素名を変更するにはXmlArrayAttributeXmlArrayItemAttributeを使用します。


public class SimpleClass
{
    [XmlArray("配列")]
    [XmlArrayItem("アイテム")]
    public int[] array = 
        new int[] { 123, 456, 789 };
}
<?xml version="1.0" encoding="utf-8"?>
<SimpleClass xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <配列>
    <アイテム>123</アイテム>
    <アイテム>456</アイテム>
    <アイテム>789</アイテム>
  </配列>
</SimpleClass>

値を要素の属性およびテキストノードにする

標準ではフィールドの値は要素の子要素として保存されます。
これを要素の属性にするにはXmlAttributeAttributeを使用します。
要素のテキストノードにするにはXmlTextAttributeを使用します。


public class SimpleClass
{
    [XmlAttribute("id")]
    public int Num;

    [XmlText]
    public string Str;

    public SimpleClass() { }

    public SimpleClass(int n, string s)
    {
        Num = n;
        Str = s;
    }
}

[XmlRoot("root")]
public class RootClass
{
    [XmlElement("simpleClass")]
    public SimpleClass sc = 
        new SimpleClass(123, "abc");
}

string path = "test.xml";

RootClass root = new RootClass();
using (StreamWriter sw = new StreamWriter(path))
{
    XmlSerializer xs = new XmlSerializer(typeof(RootClass));
    xs.Serialize(sw, root);
}

XMLは以下のようになります。

<?xml version="1.0" encoding="utf-8"?>
<root xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <simpleClass id="123">abc</simpleClass>
</root>

XmlAttributeAttributeはコンストラクターに何も指定しなければ属性名にはフィールド名がそのまま使用されます。
コンストラクターに文字列を渡すとそれが属性名となります。

テキストノードはひとつの要素につきひとつしか持てないので、XmlTextAttributeを複数のフィールドに指定するとエラーになります。

XMLの名前空間の指定

XmlRootAttributeXmlTypeAttributeXmlElementAttributeはコンストラクターで名前空間を指定することができます。
名前空間をXMLの要素に適用するにはXmlSerializerNamespacesクラスを使用します。


[XmlType(Namespace = "https://programming.pc-note.net/author")]
public class Author
{
    public string Name;
    public string Profile;
}

[XmlType(Namespace = "https://programming.pc-note.net/product")]
public class Product
{
    public string Name;
    public string Code;
}

[XmlRoot(ElementName = "root", Namespace = "https://programming.pc-note.net/")]
public class RootClass
{
    public Author[] Authors = new Author[] {
        new Author() { Name="A山B太", Profile = "あいうえお" },
        new Author() { Name="C谷D男", Profile = "かきくけこ" }
    };

    public Product[] Products = new Product[] {
        new Product() { Name="○○○", Code = "12345678" },
        new Product() { Name="×××", Code = "23456789" }
    };
}

string path = "test.xml";

RootClass rc = new RootClass();
using (StreamWriter sw = new StreamWriter(path))
{
    XmlSerializer xs = new XmlSerializer(typeof(RootClass));

    //XmlSerializerNamespacesを使用
    XmlSerializerNamespaces xsn = new XmlSerializerNamespaces();
    xsn.Add("author", "https://programming.pc-note.net/author");
    xsn.Add("product", "https://programming.pc-note.net/product");

    //第三引数にXmlSerializerNamespacesのインスタンスを指定
    xs.Serialize(sw, rc, xsn);
}
<?xml version="1.0" encoding="utf-8"?>
<root xmlns:product="https://programming.pc-note.net/product" xmlns:author="https://programming.pc-note.net/author" xmlns="https://programming.pc-note.net/">
  <Authors>
    <Author>
      <author:Name>A山B太</author:Name>
      <author:Profile>あいうえお</author:Profile>
    </Author>
    <Author>
      <author:Name>C谷D男</author:Name>
      <author:Profile>かきくけこ</author:Profile>
    </Author>
  </Authors>
  <Products>
    <Product>
      <product:Name>○○○</product:Name>
      <product:Code>12345678</product:Code>
    </Product>
    <Product>
      <product:Name>×××</product:Name>
      <product:Code>23456789</product:Code>
    </Product>
  </Products>
</root>

XmlRootAttributeなどには名前空間を引数に取るコンストラクターはないので、オブジェクト初期化子を使用して指定します。
名前空間のフィールド名は「Namespace」です。
要素名も同時に指定する場合は「ElementName」、XmlTypeAttributeの場合は「TypeName」で指定します。

XmlSerializerNamespacesクラスはAddメソッドで名前空間に対してプレフィックス(名前)を付けます。
第一引数はプレフィックスを、第二引数には名前空間を指定します。

XmlSerializerNamespacesのインスタンスをXmlSerializerのSerializeメソッドの第三引数に指定することで、指定の要素に名前空間が適用されます。

object型をシリアライズする

オブジェクトをテキストにすると型情報が失われるため、object型はそのままではXmlSerializerではシリアライズできません。
object型をXMLシリアライズするにはtypeof演算子で使用するデータ型の配列を作り、XmlSerializerのコンストラクターの第二引数に渡します。


public class SimpleClass
{
    public object Obj1;
    public object Obj2;

    public SimpleClass() { }

    public SimpleClass(object obj1, object obj2)
    {
        Obj1 = obj1;
        Obj2 = obj2;
    }
}

string path = "test.xml";

SimpleClass sc = new SimpleClass("abc", false);
using (StreamWriter sw = new StreamWriter(path))
{
    Type[] types = new Type[] { typeof(string), typeof(bool) };

    XmlSerializer xs = new XmlSerializer(typeof(SimpleClass), types);
    xs.Serialize(sw, sc);
}

XMLは以下のようになります。

<?xml version="1.0" encoding="utf-8"?>
<SimpleClass xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Obj1 xsi:type="xsd:string">abc</Obj1>
  <Obj2 xsi:type="xsd:boolean">false</Obj2>
</SimpleClass>