C# コレクションクラスのシリアライズ

初めに

とある理由でList<T>派生の独自のコレクションクラスを作成した時に、JSON/XMLへのシリアライズに問題が発生したので、調査した結果をまとめたものを記載。

問題とは、以下の3点だった。

  • コレクションクラスのメンバーが出力されない
  • 出力時に例外が発生する
  • JSONにXML文字列が出力される

コレクションクラスの構成

今回使ったクラスは、以下のようなもの。
List<T>のようなコレクションクラスに、データ保持の最大容量を示すMaxをメンバーとして持つクラス

一つ目はList<T>派生型。

class Hoge<T> : List<T>
{
 public int Max {get; set;}
}

二つ目はIEnumerable<T>/ ICollection<T>を有する独自コレクションクラス。

class Hoge<T> : IEnumerable<T>, ICollection<T>
{
 public List<T> Array {get; set; }
 public int Max {get; set; }
}

こちらの方は、Arrayのメソッドをそのまま流用しインターフェースを実装するような形にしている。

シリアライズの上位側実装

以下のようなメソッドを作成し、WriteFile(ファイル名,バッファ)でファイル出力するようにした。

public static void WriteFile<T>(string filename, T target)
{
    switch (Path.GetExtension(filename)) {
    case ".json":
        var json = new DataContractJsonSerializer(typeof(T));
        using (var stream = File.Create(filename)) {
            using (var writer = JsonReaderWriterFactory.CreateJsonWriter(stream, Encoding.UTF8, true, true)) {
                json.WriteObject(writer, target);
            }
        }
        break;
    case ".xml":
        var xml = new XmlSerializer(typeof(T));
        using (var stream = new StreamWriter(filename, false, Encoding.UTF8)) {
            xml.Serialize(stream, target);
        }
        break;
    }
}

出力方法

次の3つの方法でJSON/XMLの出力がどのように変わるかを検証。

  • クラスにDataContract属性を指定、出力メンバーにDataMember属性を指定
  • クラスにCollectionDataContract属性を指定、出力メンバーにDataMember属性を指定
  • クラスにIXmlSerializableを実装

出力結果

DataContractを使った場合

形式 JSON出力結果 XML出力結果
List<T>派生 例外が発生 <ArrayOfInt>
<int>1</int>
<int>2</int>
</ArrayOfInt>
独自 {
“Array”: [
1,
2
],
“Max”: 3
}
<RingList>
<int>1</int>
<int>2</int>
</RingList>

結果的には、望む出力ができたのは、独自クラスをJSON形式で出力した場合のみ。
それ以外は例外が出る、XML側ではメンバーが出力されない結果となった。

CollectionDataContractを使った場合

形式 JSON出力結果 XML出力結果
List<T>派生 [
1,
2
]
<ArrayOfInt>
<int>1</int>
<int>2</int>
</ArrayOfInt>
独自 [
1,
2
]
<RingList>
<int>1</int>
<int>2</int>
</RingList>

こちらは全滅。
すべてメンバーが出力されていない。

IXmlSerializableを実装した場合

形式 XML出力結果 JSON出力結果
List<T>派生 <RingListOfInt64 Max=”20″>
<long>1</long>
<long>2</long>
</FixedSizeList1OfInt64>
“<RingListOfInt64 Max=\”20\”> <long xmlns=\”\”> 1 <\/long> <long xmlns=\”\”> 2 <\/long> <\/RingListOfInt64>”
独自 <RingListOfInt64 Max=”3″>
<long>5</long>
<long>6</long>
</FixedSizeListOfInt64>
“<RingListOfInt64 Max=\”20\”> <long xmlns=\”\”> 1 <\/long> <long xmlns=\”\”> 2 <\/long> <\/RingListOfInt64>”

XMLは想定通りの出力となったが、JSONはXML文字列がそのまま出力された。

補足

複雑なクラス構成で出力を試したところ、IXmlSerializableを実装しJSON形式で出力した場合、IXmlSerializable を実装したクラスのみがXML化した文字列をJSON形式に埋め込まれていた。

{
  "Hage": 10,
  "buf1": "<FixedSizeList1Oflong Max=\"20\" xmlns=\"http:\/\/schemas.datacontract.org\/2004\/07\/Collections.Generic\"><long xmlns=\"\">1<\/long><long xmlns=\"\">2<\/long><\/FixedSizeList1Oflong>",
  "buf2": {
    "_array": [
      3,
      4,
      0,
      0
    ],
    "_head": 0,
    "_size": 2,
    "_tail": 2,
    "_version": 3
  },
  "buf3": "<FixedSizeListOflong Max=\"3\" xmlns=\"http:\/\/schemas.datacontract.org\/2004\/07\/Collections.Generic\"><long xmlns=\"\">5<\/long><long xmlns=\"\">6<\/long><\/FixedSizeListOflong>",
  "buf4": [
    7,
    8
  ]
}

結果

List<T>派生型の場合、クラスの属性としてCollectionDataContractを使わないといけなくなる。
またCollectionDataContractとした場合には、出力されるのはコレクション内のデータのみで、クラスに設定されているメンバーはどんな形でも出力はされない結果となった。

そのため、クラス内のメンバーを出力させたい場合は、以下の様にする必要があるようだ。

  • JSONでの出力は、独自クラスにしDataContract属性で設計する
  • XMLでの出力は、IXmlSerializableで実装する

DataContractとIXmlSerializableは一緒に実装できず排他関係にあるらしく、同一コードでJSON/XMLを出力しようとする場合は、IXmlSerializableを実装しXML<->JSON変換クラスを別途用意する必要があるようだ。

こちらの変換については、今回試すところまでは行っていない。

この問題が分かった当初は、google検索で答えを求めようとしたのだが、質問自体数件で、答えとしては、クラスはIEnumerable<T>/ ICollection<T>を付けない単独クラスの中にコンテナメンバーを詰めろ、といった回答で自分が求めるものではなかった。
質問内容も、「Listに制約を付けたコレクションクラスを作りたい」ではなく、「フットボールチームを表すクラスをList<T>の派生で作成したい」だったからかもしれない。

コメント

タイトルとURLをコピーしました