yotiky Tech Blog

とあるエンジニアの備忘録

C# - XmlSerializerの使い方

learn.microsoft.com

適用対象は、.NET Framework 1.1 以降。

目次

検証環境

  • LINQPad 7
  • .NET 7

XmlSerializer の使い方

サンプルで扱う PurchaseOrder.xml

<?xml version="1.0"?>
<PurchaseOrder PurchaseOrderNumber="99503" OrderDate="1999-10-20">
    <Address Type="Shipping">
        <Name>Ellen Adams</Name>
        <Street>123 Maple Street</Street>
        <City>Mill Valley</City>
        <State>CA</State>
        <Zip>10999</Zip>
        <Country>USA</Country>
    </Address>
    <Address Type="Billing">
        <Name>Tai Yee</Name>
        <Street>8 Oak Avenue</Street>
        <City>Old Town</City>
        <State>PA</State>
        <Zip>95819</Zip>
        <Country>USA</Country>
    </Address>
    <DeliveryNotes>Please leave packages in shed by driveway.</DeliveryNotes>
    <Items>
        <Item PartNumber="872-AA">
            <ProductName>Lawnmower</ProductName>
            <Quantity>1</Quantity>
            <USPrice>148.95</USPrice>
            <Comment>Confirm this is electric</Comment>
        </Item>
        <Item PartNumber="926-AA">
            <ProductName>Baby Monitor</ProductName>
            <Quantity>2</Quantity>
            <USPrice>39.98</USPrice>
            <ShipDate>1999-05-21</ShipDate>
        </Item>
    </Items>
</PurchaseOrder>

ファイルの読み書き

シリアライズ

var path = Path.Combine(Util.MyQueriesFolder, @"PurchaseOrder.xml");

using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read))
{
    var serializer = new XmlSerializer(typeof(PurchaseOrder));
    var order = (PurchaseOrder)serializer.Deserialize(stream);
}

シリアライズ

var path = Path.Combine(Util.MyQueriesFolder, @"PurchaseOrder_copy.xml");

using (var stream = new FileStream(path, FileMode.OpenOrCreate, FileAccess.Write))
{
    var serializer = new XmlSerializer(typeof(PurchaseOrder));
    serializer.Serialize(stream, order);
}

文字列の読み書き

シリアライズ

var xml = 
@"<?xml version=""1.0""?>
<PurchaseOrder PurchaseOrderNumber=""99503"" OrderDate=""1999-10-20"">
  <Address Type=""Shipping"">
      <Name>Ellen Adams</Name>
      <Street>123 Maple Street</Street>
      <City>Mill Valley</City>
      <State>CA</State>
      <Zip>10999</Zip>
      <Country>USA</Country>
  </Address>
  <Address Type=""Billing"">
      <Name>Tai Yee</Name>
      <Street>8 Oak Avenue</Street>
      <City>Old Town</City>
      <State>PA</State>
      <Zip>95819</Zip>
      <Country>USA</Country>
  </Address>
  <DeliveryNotes>Please leave packages in shed by driveway.</DeliveryNotes>
  <Items>
      <Item PartNumber=""872-AA"">
          <ProductName>Lawnmower</ProductName>
          <Quantity>1</Quantity>
          <USPrice>148.95</USPrice>
          <Comment>Confirm this is electric</Comment>
      </Item>
      <Item PartNumber=""926-AA"">
          <ProductName>Baby Monitor</ProductName>
          <Quantity>2</Quantity>
          <USPrice>39.98</USPrice>
          <ShipDate>1999-05-21</ShipDate>
      </Item>
  </Items>
</PurchaseOrder>";

using (var stream = new StringReader(xml))
{
    var serializer = new XmlSerializer(typeof(PurchaseOrder));
    var order = (PurchaseOrder)serializer.Deserialize(stream);
}

シリアライズ

using (var stream = new StringWriter())
{
    var serializer = new XmlSerializer(typeof(PurchaseOrder));
    serializer.Serialize(stream, order);
    stream.ToString().Dump();
}

属性

XMLのデータ型。

// タグ名と同じならあってもなくてもOK
[XmlRoot]
public class Book
{
    // タグ名と同じならあってもなくてもOK
    [XmlElement]
    public int Id { get; set; }

    public string Title { get; set; }
    
    // フィールでも出力される
    public int Price;
    
    // タグ名が違う場合は明示的に指定
    [XmlElement("Published")]
    public DateTime published;

    [XmlAttribute]
    public string ISBN13 { get; set; }

    public BookType BookType { get; set; }

    public BookType BookType { get; set; }
    
    public Genre Genre { get; set; }

    public Publisher Publisher { get; set; }
    
    public List<Author> Authors { get; set; }

    // コレクションのタグ指定
    [XmlArray("Languages")]
    [XmlArrayItem("Language")]
    public List<string> Langs { get; set;}

    // コレクションの親要素抜きでベタに並べる場合
    [XmlElement("Seller")]
    public List<Seller> Sellers { get; set;}
    // Dictionaryは使えない
    //public Dictionary<string, string> NotSupported { get; set;}
}

public class Author
{
    public string Name { get; set; }
}

public class Publisher
{
    public string Name { get; set; }
}

public class Seller
{
    // <Name>タグで囲わず、<Publisher>タグのテキストとして出力する
    [XmlText]
    public string Name { get; set; }
}

public enum BookType
{
    PaperBook,
    EBook,
}

public enum Genre
{
    // enumの出力する値を指定できる
    [XmlEnum("コミック")]
    Comic,
    [XmlEnum("ラノベ")]
    LightNovel,
}

シリアライズとデシリアライズ

var book = new Book
{
    Id = 1,
    Title = "薬屋でひとりごっこ",
    Price = 777,
    published = new DateTime(2000, 1, 2),
    Authors = new List<Author>
    {
        new Author{ Name = "春夏冬" },
        new Author{ Name = "洞爺湖" },
        new Author{ Name = "京都三ノ宮" },
    },
    Publisher = new Publisher { Name = "中学館" },
    Langs = new List<string> { "ja", "en" },
    Sellers = new List<Seller> {
        new Seller{ Name = "あまぞん" },
        new Seller{ Name = "よどばし" },
    },
    BookType = BookType.PaperBook,
    Genre = Genre.Comic,
};
var bookXml = "";

using (var stream = new StringWriter())
{
    var serializer = new XmlSerializer(typeof(Book));
    serializer.Serialize(stream, book);
    bookXml = stream.ToString();
}

using (var stream = new StringReader(bookXml))
{
    var serializer = new XmlSerializer(typeof(Book));
    var deserialized = (Book)serializer.Deserialize(stream);

    Util.OnDemand("AttributePattern", () => Util.HorizontalRun(true, bookXml, deserialized)).Dump();
}

実行結果。

XmlRoot / XmlType

どちらもクラスに適用する属性だが、XmlRootはルート要素を表すクラス1つのみに適用できる。(子要素に複数適用できない) クラス名と同じXML要素を出力する場合は、属性が無くても問題ない。

[XmlRoot]
public class Book
{
}

public class Publisher
{
}

[XmlType("html")]
public class Html
{
}


これらは、XML スキーマ定義ツールを使った場合に以下の違いが発生する。

<xs:element name="NewGroupName" type="NewTypeName" />

XmlSerializer に渡すルートのクラスとXML要素名が違う場合は、XmlSerializerのコンストラクタでXmlRootを指定できる。

var publisherDiffXml = "";

using (var stream = new StringWriter())
{
    var serializer = new XmlSerializer(typeof(Publisher), new XmlRootAttribute("PublisherRoot"));
    serializer.Serialize(stream, publisher);
    publisherDiffXml = stream.ToString();
}

using (var stream = new StringReader(publisherDiffXml))
{
    var serializer = new XmlSerializer(typeof(Publisher), new XmlRootAttribute("PublisherRoot"));
    var deserialized = (Publisher)serializer.Deserialize(stream);

    Util.OnDemand("AttributePattern XmlRoot Diff", () => Util.HorizontalRun(true, publisherDiffXml, deserialized)).Dump();
}

XmlElement

メンバに適用する属性。メンバは子要素として扱われる。 メンバ名とXML要素を出力する場合は、属性が無くても問題ない。 パブリックなプロパティとフィールドどちらも対象で、フィールが先にXMLに出力される。

[XmlElement]
public int Id { get; set; }

public string Title { get; set; }

public int Price;

[XmlElement("Published")]
public DateTime published;

XmlAttribute

メンバに適用する属性。XML属性として扱われる。 XML属性にする場合はXmlAttribute必須、XML属性名を変える場合は引数で指定。

[XmlAttribute("ISBN10")]
public string ISBN { get; set; }

[XmlAttribute]
public string ISBN13 { get; set; }

XmlIgnore

XMLに入出力されなくなる。

[XmlIgnore]
public int Attribute { get; set; }

XmlEnum

enumのメンバに適用すると、XMLに出力する値を指定できる。

public enum BookType
{
    PaperBook,
    EBook,
}

public enum Genre
{
    [XmlEnum("コミック")]
    Comic,
    [XmlEnum("ラノベ")]
    LightNovel,
}

XmlText

メンバに適用すると、メンバ名に対する要素が出力されず親要素の値として出力される。

public class Seller
{
    [XmlText]
    public string Name { get; set; }
}

XmlArray / XmlArrayItem

コレクションのメンバに適用する。 メンバ名とXML要素を出力する場合は、属性が無くても問題ない。 XmlArrayで親要素、XmlArrayItemで子要素のXML要素名を指定できる。 DictionaryXmlSerializerがサポートしていないので注意が必要。

public List<Author> Authors { get; set; }

[XmlArray("Languages")]
[XmlArrayItem("Language")]
public List<string> Langs { get; set;}

// Dictionaryは使えない
//public Dictionary<string, string> NotSupported { get; set;}

子要素をベタに並べる

親要素なしでベタに並べる場合はXmlElementを適用する。

[XmlElement("Seller")]
public List<Seller> Sellers { get; set;}

不特定多数の子要素

子要素が不特定多数のXML要素で成り立っている場合、XmlElementTypeの引数を指定する。

[XmlType("html")]
public class Html
{
    [XmlElement("div", typeof(Div))]
    [XmlElement("p", typeof(P))]
    public List<object> Items { get; set; }
    
    public class Div
    {
        [XmlText]
        public string Text { get; set; }
    }
    public class P
    {
        [XmlText]
        public string Value { get; set; }
    }
}
var mixed = new Html
{
    Items = new List<object>
    {
        new Html.Div { Text = "Hoge" },
        new Html.P { Value = "Fuga" },
        new Html.P { Value = "PiyoPiyo" },
    }
};
var mixedXml = "";

using (var stream = new StringWriter())
{
    var serializer = new XmlSerializer(typeof(Html));
    serializer.Serialize(stream, mixed);
    mixedXml = stream.ToString();
}

using (var stream = new StringReader(mixedXml))
{
    var serializer = new XmlSerializer(typeof(Html));
    var deserialized = (Html)serializer.Deserialize(stream);

    Util.OnDemand("MixedList", () => Util.HorizontalRun(true, mixedXml, deserialized)).Dump();
}

フォーマット

nullの扱い

XML属性

XmlAttribute単体では Nullable を扱えない。 stringの代替プロパティを用意して、Nullable のプロパティはXmlIgnoreにして対応する。

// XmlAttribute は Nullable 使えない
[XmlAttribute]
public int Attribute { get; set; }

// XmlAttribute の Nullable は文字列の代替プロパティで回避
[XmlIgnore]
public int? Attribute2 { get; set; }

[XmlAttribute("Attribute2")]
public string Attribute2String
{
    get
    {
        return this.Attribute2?.ToString().ToLower();
    }
    set
    {
        this.Attribute2 = value != null ? int.Parse(value) : null;
    }
}

XML要素

NullableのXmlElementでnullだった場合、<Element xsi:nil="true" />が出力される。
nullの場合にXML要素自体出力しないようにするには、bool型の「プロパティ名+Specified」プロパティを定義すると、falseの時は出力されなくなる。空タグの制御もこれで可能。

Browsable(false)を適用するとインテリセンスに出なくなる。

// XmlElement は null の場合、「<Element xsi:nil="true" />」が出力される
[XmlElement]
public int? Element { get; set; }

// null の場合にタグを出力しないようにするには、「プロパティ名+Specified」プロパティで制御する
public int? Element2 { get; set;}
[XmlIgnore, Browsable(false)]
public bool Element2Specified => Element2.HasValue;

出力フォーマット

XmlElementDataType引数でちょっとしたフォーマットの指定くらいはできるが、細かくフォーマットを指定したい場合は、stringの代替プロパティを用意する。

[XmlElement]
public DateTime FixedDateTimeRaw { get; set; }

[XmlElement(DataType = "date")]
public DateTime FixedDate { get; set; }

[XmlElement(DataType = "dateTime")]
public DateTime FixedDateTime { get; set; }

[XmlElement(DataType = "time")]
public DateTime FixedTime { get; set; }

// DateTimeなど出力のフォーマットを調整する場合も文字列の代替プロパティを使う
[XmlIgnore]
public DateTime SomeDate { get; set; }
[XmlElement("SomeDate")]
public string SomeDateString
{
    get
    {
        return this.SomeDate.ToString("yyyy/MM/dd HH:mm:ss.fff");
    }
    set
    {
        this.SomeDate = DateTime.Parse(value);
    }
}

オプション

XML宣言にUTF-8を指定する

XmlSerializer はデフォルトだと、XML宣言にutf-16を設定する。

utf-8を設定する場合は直接指定できないため、StringWriter を継承したクラスを自作する必要がある。

public class Utf8StringWriter : StringWriter
{
    public override Encoding Encoding => Encoding.UTF8;
}

private void WriteStringWithUTF8(PurchaseOrder order)
{
    using (var stream = new Utf8StringWriter())
    {
        var serializer = new XmlSerializer(typeof(PurchaseOrder));
        serializer.Serialize(stream, order);

        Util.OnDemand("WriteStringWithUTF8", () => stream.ToString()).Dump();
    }
}

XML宣言を出力しない

var settings = new XmlWriterSettings
{
    // XML宣言除去
    OmitXmlDeclaration = true,
};

using (var stream = new StringWriter())
using (var writer = XmlWriter.Create(stream, settings))
{
    var serializer = new XmlSerializer(typeof(PurchaseOrder));
    serializer.Serialize(writer, order);

    Util.OnDemand("WriteStringWithOption", () => stream.ToString()).Dump();
}

名前空間の宣言を出力しない

// 名前空間の宣言除去
var namespaces = new XmlSerializerNamespaces();
namespaces.Add("", "");

using (var stream = new StringWriter())
using (var writer = XmlWriter.Create(stream))
{
    var serializer = new XmlSerializer(typeof(PurchaseOrder));
    serializer.Serialize(writer, order, namespaces);

    Util.OnDemand("WriteStringWithOption", () => stream.ToString()).Dump();
}

インデントを付ける

var settings = new XmlWriterSettings
{
    // XML宣言除去
    OmitXmlDeclaration = true,
    // インデント付き
    Indent = true,
    // 1つのインデントに使う文字列
    IndentChars = "    ",
};
// 名前空間の宣言除去
var namespaces = new XmlSerializerNamespaces();
namespaces.Add("", "");

using (var stream = new StringWriter())
using (var writer = XmlWriter.Create(stream, settings))
{
    var serializer = new XmlSerializer(typeof(PurchaseOrder));
    serializer.Serialize(writer, order, namespaces);

    Util.OnDemand("WriteStringWithOption", () => stream.ToString()).Dump();
}