1. はじめに
.NET はデフォルトで公開フィールドやプロパティに基づいてオブジェクトをXMLにシリアライズ/デシリアライズできるのは知ってるよね。でも、綺麗なXMLを出したいとか、他のプログラムや標準と合わせたい、あるいは内部の詳細をXMLに含めたくない場合は細かい調整が必要になる。
C# にはそのための シリアライズ属性 があって、クラスやプロパティ、フィールドに「タグ」を付ける感じで制御するんだ。これらの属性を使えば、どのフィールドがXMLに出るか、何を無視するか、要素名をどうするか、属性にするか、複雑な構造にするか、要素の順序まで、ほぼすべてをコントロールできる。
デフォルトのシリアライズは自動で用紙に記入されるようなものだとすると、属性を使うと君はスーパーカスタムな事務官みたいに、どの行に何を書くか、何を飛ばすか、何を強調するかを自由に決められるんだ。
オブジェクトとXMLの対応
| クラス/プロパティ | 属性 | XMLでの結果 |
|---|---|---|
|
クラス UserList | |
|
|
|
|
|
id="..." (属性) |
|
|
|
|
|
|
|
[XmlIgnore] (RegisteredAtStringを参照) | - |
|
|
|
|
|
|
2. XML シリアライズ属性の概要
ここでは長年.NETユーザーがクラスに付けてきた、もっともポピュラーな属性をざっと見ていこう!
属性 [XmlElement] — 要素名
XMLでの要素名を変更する、またはプロパティ/フィールドをXML要素としてマークする。
public class Person
{
[XmlElement("FullName")]
public string Name { get; set; }
}
XML:
<Person>
<FullName>ヴァシリー・ペトロフ</FullName>
</Person>
属性を付けなければ、要素名はデフォルトでプロパティ/フィールド名と同じになるよ。
属性 [XmlAttribute] — XML属性としてシリアライズ
プロパティ/フィールドを要素ではなくXML属性としてシリアライズできる。
public class Person
{
[XmlAttribute("id")]
public int Id { get; set; }
}
XML:
<Person id="123"></Person>
実際のXML APIでは、識別子や日付、フラグなどに属性を使うことが多いね。
属性 [XmlIgnore] — フィールド/プロパティをスキップ
これはパラノイア向けの夢機能:プロパティをシリアライズ/デシリアライズから完全に除外できる!この属性が付いたものは最終的なXMLに一切出ない。
public class Person
{
[XmlIgnore]
public string InternalNote { get; set; }
}
XML:(InternalNote プロパティはここには存在しないし、今後も出ない)
属性 [XmlArray] と [XmlArrayItem] — コレクションの制御
配列やコレクション専用の属性。[XmlArray] が外側のラッパー名を指定し、[XmlArrayItem] が個々の要素名を指定する。
public class Person
{
[XmlArray("Phones")]
[XmlArrayItem("Phone")]
public string[] PhoneNumbers { get; set; }
}
XML:
<Person>
<Phones>
<Phone>+12951234567</Phone>
<Phone>+12876543210</Phone>
</Phones>
</Person>
属性 [XmlRoot] — ルート要素名
クラス自体に付けて、XMLのルート要素名を変える。
[XmlRoot("User")]
public class Person
{
public string Name { get; set; }
}
XML:
<User>
<Name>アンナ</Name>
</User>
属性 [XmlText] — 要素のテキスト内容としてシリアライズ
仕様上、プロパティの値を要素の中のテキストだけにしたい(サブ要素でも属性でもない)場合がある。
public class Note
{
[XmlText]
public string Content { get; set; }
}
XML:
<Note>おばあちゃんに電話して!</Note>
属性 [XmlNamespaceDeclarations], [XmlElement(Type = ...)]
名前空間を扱ったり、継承をサポートしたりするより高度なシナリオ向けの属性もある。完全な一覧は XMLシリアライズの公式ドキュメント をチェックしてね。
3. 実践:実際の例でシリアライズを改善する
ステップごとに学習用アプリを「XMLシリアライズのプレミアム版」まで改良して、厳しいXMLインスペクタも満足するようにしよう。
属性なしの基本クラス
例えば、こんなユーザークラスがあるとする:
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string[] Emails { get; set; }
public DateTime RegisteredAt { get; set; }
public string Password { get; set; }
}
デフォルトのシリアライズはだいたいこんなXMLを出す(日時も独自フォーマットで出る):
<User>
<Id>42</Id>
<Name>Ivan Ivanov</Name>
<Emails>
<string>user@mail.com</string>
<string>admin@site.org</string>
</Emails>
<RegisteredAt>2024-07-01T00:00:00</RegisteredAt>
<Password>qwerty</Password>
</User>
問題点:
- パスワードはシリアライズしない方がいい。
- 日付は属性にして人間が読める形式にしたい。
- Emails を綺麗な形にしたい。
- ルート要素を <Person> に変えたい。
属性で設定を追加する
[XmlRoot("Person")]
public class User
{
[XmlAttribute("id")]
public int Id { get; set; }
[XmlElement("FullName")]
public string Name { get; set; }
[XmlArray("EMails")]
[XmlArrayItem("Email")]
public string[] Emails { get; set; }
[XmlAttribute("registered")]
public string RegisteredAtString
{
get => RegisteredAt.ToString("yyyy-MM-dd");
set => RegisteredAt = DateTime.Parse(value);
}
[XmlIgnore]
public string Password { get; set; }
[XmlIgnore]
public DateTime RegisteredAt { get; set; }
}
細かい説明:
- [XmlAttribute("registered")] を使って日付を属性として出力している。
- DateTime 自体をシリアライズするのではなく文字列を出したいので、別プロパティ RegisteredAtString を用意し、実際のフィールドは [XmlIgnore] で隠している。
- パスワードは [XmlIgnore] で隠している。
- メール配列は外側タグを <EMails>、各要素を <Email> にしている。
結果:
<Person id="42" registered="2024-07-01">
<FullName>Ivan Ivanov</FullName>
<EMails>
<Email>user@mail.com</Email>
<Email>admin@site.org</Email>
</EMails>
</Person>
4. ネストしたオブジェクトの扱い
ユーザーとその住所のようなネストされたオブジェクトがあると、XMLは真価を発揮する。
public class Address
{
[XmlAttribute]
public string City { get; set; }
[XmlText]
public string Details { get; set; }
}
public class User
{
// ...他のプロパティは上を参照...
public Address[] Addresses { get; set; }
}
住所配列に対する設定を追加しよう:
[XmlArray("UserAddresses")]
[XmlArrayItem("Address")]
public Address[] Addresses { get; set; }
その場合のXMLはこんな感じになる:
<Person id="42" registered="2024-07-01">
<FullName>Ivan Ivanov</FullName>
<EMails>
<Email>user@mail.com</Email>
</EMails>
<UserAddresses>
<Address City="Neon city">ザムコフスカヤ通り 1</Address>
<Address City="North Cave">クライニャヤ通り 12</Address>
</UserAddresses>
</Person>
各 <Address> の City は属性になっていて、住所の本文は要素のテキストになっているのが分かるね。
5. よくあるミスと注意点
複雑なオブジェクトをシリアライズして期待したXMLが出ない場合、属性の付け忘れが原因であることが多い。時々プロパティやフィールドにマーキングが残っていないことがある。そうなると .NET はデフォルトの挙動を使ってしまう。
もう一つの落とし穴はコレクションの名前。配列(例えば string の配列)が [XmlArray] も [XmlArrayItem] も付いていないと、要素は string タグで出力される(冒頭の例を見て)。これを避けるには常に明示的にタグ名を属性で指定しよう。
「getter-only」プロパティ(読み取り専用)はデフォルトではデシリアライズされない点にも注意。可能なら public な set を用意しておくと楽になる。
辞書(dictionary)やインターフェースのような型は標準の XmlSerializer では直接シリアライズできないか、別途処理が必要になるケースがある。
GO TO FULL VERSION