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であること
  • 引数なしコンストラクターがあること
    (自動生成されるやつでも良い)

シリアライズするクラスのフィールドまたはプロパティに、別のクラス(または構造体)が含まれる場合、そのクラスの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はシリアル化したデータの書き込み先にStream派生クラス、またはStreamWriterクラス(正確にはTextWriter派生クラス)を使用します。
ここでは外部ファイルに保存するためにStreamWriterを使用します。


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

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

実際にシリアライズするには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メソッドを使用します。


SimpleClass sc;
using (StreamReader sr = new StreamReader("test.xml"))
{
    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;
    }
}

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

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

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

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

フィールドの追加

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

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

XMLのタグ名のカスタマイズ

XMLのタグ名は標準ではクラス名やフィールド名がそのまま使用されます。
(タグ名: <tag>あいうえお</tag>の「tag」の部分の名前)
外部から編集される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]);
    }
}

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

using (StreamWriter sw = new StreamWriter("test.xml"))
{
    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 };
}

SimpleClass sc = new SimpleClass();
using (StreamWriter sw = new StreamWriter("test.xml"))
{
    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>

配列のタグ名や値のタグ名を変更するにはXmlArrayAttribute属性、XmlArrayItemAttribute属性を使用します。


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>

なお、ひとつのフィールドまたはプロパティに複数の属性をつける場合、上記のように属性を連続して記述するほか、属性指定の角括弧内でコンマで区切って記述することもできます。


public class SimpleClass
{
    [XmlArray("配列"),
    XmlArrayItem("アイテム")]
    public int[] array;
}

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

標準ではフィールドの値は要素の子要素として保存されます。
これを要素の属性にするには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");
}

RootClass root = new RootClass();
using (StreamWriter sw = new StreamWriter("test.xml"))
{
    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" }
    };
}

RootClass rc = new RootClass();
using (StreamWriter sw = new StreamWriter("test.xml"))
{
    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プロパティ)を引数に取るコンストラクターはありません。
しかしpublicなフィールドやプロパティに対してはフィールド名 = 値という形で名前付き引数を使用して値を割り当てることができます。
(属性クラスだけで可能な書式です)

タグ名も同時に指定する場合はElementNameXmlTypeAttribute属性の場合はTypeNameを名前付き引数で指定します。

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

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

object型および派生クラスのシリアライズ

XmlSerializerでシリアライズする対象のフィールドおよびプロパティにobject型が含まれる場合、そこにユーザー定義クラス(または構造体)が保存されているとシリアライズに失敗します。
また、派生クラスのインスタンスを基底クラス型で受け取っている場合(アップキャスト)もシリアライズに失敗します。


public class MyBase { public int Num; }
public class MyDerived : MyBase { public string Str; }

public class SimpleClass
{
    public object Obj;
    public MyBase Base;
}

SimpleClass sc = new SimpleClass()
{
    //object型にユーザー定義クラスを代入
    Obj = new MyBase()
    {
        Num = 123,
    },
    //基底クラス型に派生クラスを代入
    Base = new MyDerived()
    {
        Num = 456,
        Str = "abc"
    }
};
using (StreamWriter sw = new StreamWriter("test.xml"))
{
    XmlSerializer xs = new XmlSerializer(typeof(SimpleClass));

    //ここでエラー
    xs.Serialize(sw, sc);
}

このようなクラスをシリアライズ可能にするには二通りの方法があります。

使用するデータ型をコンストラクターで指定する

シリアライズ対象に含まれるユーザー定義型のTypeオブジェクトを取得し、その配列をXmlSerializerのコンストラクターの第二引数に渡すことでシリアライズ可能になります。
データ型のTypeクラスオブジェクトはtypeof演算子で取得できます。


public class MyBase { public int Num; }
public class MyDerived : MyBase { public string Str; }

public class SimpleClass
{
    public object Obj;
    public MyBase Base;
}

SimpleClass sc = new SimpleClass()
{
    Obj = new MyBase()
    {
        Num = 123,
    },
    Base = new MyDerived()
    {
        Num = 456,
        Str = "abc"
    }
};
using (StreamWriter sw = new StreamWriter("test.xml"))
{
    //保存するデータ型のTypeオブジェクトをtypeof演算子で取得し、
    //配列にする
    Type[] types = new Type[] { typeof(MyBase), typeof(MyDerived) };

    //第二引数にType型配列を指定
    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">
  <Obj xsi:type="MyBase">
    <Num>123</Num>
  </Obj>
  <Base xsi:type="MyDerived">
    <Num>456</Num>
    <Str>abc</Str>
  </Base>
</SimpleClass>

このXMLデータをデシリアライズする場合も同様に、XmlSerializerの第二引数にTypeオブジェクトの配列を渡してからDeserializeメソッドを実行します。

int型などの組み込み型のTypeオブジェクトは指定する必要はありません。
また、列挙型は整数型(列挙型の基底の型)として保存されるので、Typeオブジェクトを指定する必要はありません。

ちなみに使用するTypeオブジェクトが一つだけの場合でも、要素数がひとつの配列にして渡す必要があります。

クラス側で属性を指定する

シリアライズ対象のクラスのフィールド(プロパティ)にXmlElementAttribute属性を指定することでもシリアライズ可能になります。


public class MyBase { public int Num; }
public class MyDerived : MyBase { public string Str; }

public class SimpleClass
{
    //MyBase型としてシリアライズ
    [XmlElement(typeof(MyBase))]
    public object Obj;

    //MyDerive型としてシリアライズ
    [XmlElement(typeof(MyDerived))]
    public MyBase Base;
}

SimpleClass sc = new SimpleClass()
{
    Obj = new MyBase()
    {
        Num = 123,
    },
    Base = new MyDerived()
    {
        Num = 456,
        Str = "abc"
    }
};
using (StreamWriter sw = new StreamWriter("test.xml"))
{
    XmlSerializer xs = new XmlSerializer(typeof(SimpleClass));
    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">
  <Obj>
    <Num>123</Num>
  </Obj>
  <Base>
    <Num>456</Num>
    <Str>abc</Str>
  </Base>
</SimpleClass>

XmlElementAttribute属性でフィールド(プロパティ)にデータ型を指定した場合、その指定したデータ型以外が保存されているとシリアライズに失敗します。
基底クラス型のフィールドであっても、基底クラスが保存されているとシリアライズできません。
(フィールドに代入すること自体は可能です)

XmlElementAttribute属性はXMLタグ名を指定する場合にも使用するので、両方を指定するには名前付き引数を使用します。


public class SimpleClass
{
    //タグ名は"オブジェクト"
    //MyBase型としてシリアライズ
    [XmlElement(ElementName = "オブジェクト", Type = typeof(MyBase))]
    public object Obj;
}

ElementNameプロパティがタグ名を、Typeプロパティがデータ型を指定します。

複数のデータ型の指定と注意点

先ほどの派生クラスを受け取れるようにしたフィールドBaseは、XmlElementAttribute属性で派生クラスのデータ型を指定しています。
この場合、フィールドのデータ型は基底クラスであるにもかかわらず基底クラスが保存されている状態ではシリアライズができません。
これを基底クラスが保存されている場合でもシリアライズ可能にするには、XmlElementAttribute属性を複数指定します。



public class MyBase { public int Num; }
public class MyDerived : MyBase { public string Str; }

public class SimpleClass
{
    //MyBase型とMyDerived型をシリアライズ可能
    [XmlElement(typeof(MyBase))]
    [XmlElement(typeof(MyDerived))]
    public MyBase Base;
}


SimpleClass sc = new SimpleClass()
{
    Base = new MyBase()
    {
        Num = 123
    }
};
using (StreamWriter sw = new StreamWriter("test.xml"))
{
    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">
  <MyBase>
    <Num>123</Num>
  </MyBase>
</SimpleClass>

これで基底クラスと派生クラスのどちらが保存されていてもシリアライズ可能になるのですが、出力されたXMLデータを見るとタグ名が「MyBase」となっていることが分かります。
いままでは特に指定しない場合はタグ名にはフィールド(プロパティ)名がそのまま使用されていましたが、XmlElementAttribute属性で複数のデータ型をシリアライズ可能にすると、実際に使用されたデータ型がタグ名に記述されます。

そのため複数のフィールドに対して同じデータ型を指定した場合、タグ名が重複します。


//このクラスはシリアライズ不可
public class SimpleClass
{
    [XmlElement(typeof(byte))]
    [XmlElement(typeof(int))]
    public object Obj1;

    [XmlElement(typeof(byte))]
    [XmlElement(typeof(int))]
    public object Obj2;
}

このクラスのフィールドはタグ名が「byte」もしくは「int」で同じになる可能性があります。
タグ名が重複することはXMLの仕様上は問題ありませんが、クラスノード(XMLタグ)の子ノードはフィールド(またはプロパティ)を意味します。
つまりメンバ名が重複するクラスを定義することになってしまうので、シリアライズができません。

これをシリアライズ可能にするには、名前付き引数を使用してタグ名を明示的に指定します。


public class SimpleClass
{
    [XmlElement(ElementName = "Obj1_byte", Type = typeof(byte))]
    [XmlElement(ElementName = "Obj1_int", Type = typeof(int))]
    public object Obj1;

    [XmlElement(ElementName = "Obj2_byte", Type = typeof(byte))]
    [XmlElement(ElementName = "Obj2_int", Type = typeof(int))]
    public object Obj2;
}

SimpleClass sc = new SimpleClass()
{
    Obj1 = 123,
    Obj2 = (byte)456,
};
using (StreamWriter sw = new StreamWriter("test.xml"))
{
    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">
  <Obj1_int>123</Obj1_int>
  <Obj2_byte>456</Obj2_byte>
</SimpleClass>

配列の場合

object型の配列、または派生クラスを受け取る基底クラス型の配列の場合はXmlArrayItemAttribute属性でシリアライズ対象にするデータ型を指定します。


public class SimpleClass
{
    [XmlArrayItem(typeof(int))]
    [XmlArrayItem(typeof(string))]
    public object[] Objs;

    [XmlArrayItem(typeof(MyBase))]
    [XmlArrayItem(typeof(MyDerived))]
    public MyBase[] Bases;
}

SimpleClass sc = new SimpleClass()
{
    Objs = new object[] {
        123,
        "abc"
    },
    Bases = new MyBase[] {
        new MyBase(){ Num = 456 },
        new MyDerived(){ Num = 789, Str = "def" }
    }
};
using (StreamWriter sw = new StreamWriter("test.xml"))
{
    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">
  <Objs>
    <int>123</int>
    <string>abc</string>
  </Objs>
  <Bases>
    <MyBase>
      <Num>456</Num>
    </MyBase>
    <MyDerived>
      <Num>789</Num>
      <Str>def</Str>
    </MyDerived>
  </Bases>
</SimpleClass>

属性でデータ型を指定するほか、使用するデータ型をコンストラクターで指定する方法でも構いません。

なお、System.Collections.ArrayListというクラスも値をobject型として保存するので、同様にデータ型を明示する必要があります。