C#::ポリモーフィックな型のXMLシリアライズ(1)

XmlSerializerでポリモーフィックな型のシリアライズに挑戦だ!
ちょっと前にswitch/caseして〜なんて書いたんですけど(id:lord_hollow:20090611:p1)、詳しく調べてみるとどうもそんなことしなくても大丈夫そうなので、その方法について書きます。
まずは、シリアライズに使用する型の定義から。

public class BasicClass{
  public string Name;
}
public class InheritedClassA : BasicClass{
  public int Value;
}
public class InheritedClassB : BasicClass{
  public string String;
}
public class ClassList{
  public BasicClass Owner;
  public List<BasicClass> Children;
}

これを使ってインスタンスを準備。

ClassList list = new ClassList {
  Owner = new InheritedClassB { Name = "オーナー", String = "いぶし銀" },
  Children = new List<BasicClass>{
    new BasicClass { Name = "アイテム1" },
    new InheritedClassA { Name = "アイテムA", Value = 25 },
    new InheritedClassB { Name = "アイテムB", String = "文字列" },
  }
};

C#3.0のクラス初期化機能って便利ですねぇ。
で、これをシリアライズ/デシリアライズするのは以下のコード。

const string filename = "sample.xml";
var ser = new XmlSerializer(list.GetType());

using (var stw = new StreamWriter(filename)){
  ser.Serialize(stw, list);
}

ClassList listFromXml = null;
using (var str = new StreamReader(filename)){
  listFromXml = ser.Deserialize(str) as ClassList;
}

最終的に変換と再変換が正常に終了すれば、listFromXmlが最初のlistと同じ内容になるはずなのです。
とりあえず、これを実行すると・・・Serializeでエラーになります。ちなみに、listにBasicClassのインスタンスだけを登録してあればエラーにはなりません。この事から、サブクラスの識別ができていないことが予想されます。
この解決法として、Typeを指定したXmlElementやXmlArrayItem属性を使用するといいらしいです。

public class ClassList{
  [XmlElement(typeof(BasicClass))]
  [XmlElement(typeof(InheritedClassA))]
  [XmlElement(typeof(InheritedClassB))]
  public BasicClass Owner;

  [XmlArrayItem(typeof(BasicClass))]
  [XmlArrayItem(typeof(InheritedClassA))]
  [XmlArrayItem(typeof(InheritedClassB))]
  public List<BasicClass> Children;
}

使用される可能性のある型すべてを列挙する必要があります。この属性をつけた状態でシリアライズした結果は、以下のようなツリーになります。

ClassList
  InheritedClassB
    Name
    String
  Children
    BasicClass
      Name
    InheritedClassA
      Name
      Value
    InheritedClassB
      Name
      String

OwnerプロパティはXmlElementの効果によって名前が型名に変わります。これにより、複数の型に対応できるわけですね。Childrenについても同様。
ここで試しにOwner側の属性を全部削除してみると・・・例外になるような気がするんですが、実はちゃんと動きます。OwnerからXmlElementがなくなった影響で名前はOwnerのままになり、かわりにxsi:type属性が追加されることで型が識別されます。だからといって、Childrenの定義そのものをなくす(ClassListにOwnerだけを定義)しても、OwnerがInheritedClassAだったりすると例外が出るから摩訶不思議ですな。

ちなみに、出力されるXMLのツリーを以下のようにするには・・・

ClassList
  Owner xsi:type="InheritedClassB"
    Name
    String
  BasicClass
    Name
  InheritedClassA
    Name
    Value
  InheritedClassB
    Name
    String

ClassListの各プロパティの属性を以下のように設定します。

public class ClassList
{
  public BasicClass Owner;
  [XmlElement(typeof(BasicClass))]
  [XmlElement(typeof(InheritedClassA))]
  [XmlElement(typeof(InheritedClassB))]
  public List<BasicClass> Children;
}

IEnumerableなプロパティに対してXmlElement属性を付与すると、子ノードとして直接追加されていくようになります。Ownerが存在するので変な感じですけど。Ownerがstringか何かであれば、[XmlAttribute]を使ってClassListの属性に出来るので違和感は減少するかと思われます。