【C#】オブジェクトの内容をXMLへ書き出す・読みこむ

オブジェクトの内容を、XmlSerializerを使ってXMLへ読み(デシリアライズ)書き(シリアライズ)する方法のまとめです。

単純な読み書き程度はクラスさえ用意すれば数行で読み書きができます。また、標準機能を使用を使用するので特別な外部ライブラリは必要ありません。すべて.NET Frameworkの標準の機能で読み書きできます。かなり古くからある機能なので、知っていれば簡単なXML読み書き(のような、あまり面白くない作業)からかなり解放されます。

プロジェクトの準備

XmlSerializer を使用するためにプロジェクトの参照設定に System.Runtime.Serializationを 追加します(.NET Coreでは不要です)

f:id:Takachan:20170622223710p:plain

f:id:Takachan:20170622223728p:plain

はじめに

まず XML シリアライズで使用する 4 つの属性です。XmlSerializer は XML を読み書きする時に、オブジェクトに付与されている属性によって処理が変化します。

以下の属性は setter/getter が public な属性もしくは public なフィールドで使用できます。逆に private フィールドや getter しかないプロパティでは付与しても正しく処理されません。

// ルートタグを指定。一番先頭のクラスに指定できる。
[XmlRoot("root")]

// 要素名の指定。プロパティに指定できる。
[XmlElement("elem")]

// 属性名の指定。これもプロパティに指定できる。
[XmlAttribute("att")]

// XMLへ読み書きしない指定。プロパティに指定可能。
[XmlIgnore]

各属性は名前を指定しない場合プロパティやフィールドの名前がXMLの要素や属性の名前になる。
** たとえ同じ名前だったとしても名前は基本的に指定しておいた方がよい

名前を省略した場合プロパティ名が要素名として認識されます。

実装例

使用するXML

今回は以下のXMLを読み書きしたいと思います。

<root>
  <sub att = "att1">
    <text>str1</text>
    <value-i>111</value-i>
    <value-d>0.123</value-d>
  </sub>
  <sub att="att1">
    <text>str1</text>
    <value-i>222</value-i>
    <value-d>9.876</value-d>
  </sub>
  <list>
    <member no="1">mmmm</member>
    <member no="2">qqqq</member>
  </list>
</root>

クラス定義

サンプルXMLを読み取るために、XML構造に対応したクラスを用意し、メンバーに冒頭で紹介した属性を付与していきます。

[XmlRoot("root")]
public class Root
{
    [XmlElement("sub")]
    public List<Sub> SubList { get; set; } = new List<Sub>(); // 複数要素の場合リスト

    [XmlElement("list")]
    public MemberList MemberList { get; set; } = new MemberList();
}
public class Sub
{
    // 属性の定義
    [XmlAttribute("att")]
    public string Attributte { get; set; } = "";

    // 子要素の定義
    [XmlElement("text")]
    public string StringNode { get; set; } = "";

    [XmlElement("value-i")]
    public int ValueInt { get; set; } // 整数として読み取り

    [XmlElement("value-d")]
    public double ValueDouble { get; set; } // 浮動小数点として読み取り
}
public class MemberList
{
    [XmlElement(ElementName = "member")]
    public List<Member> Members { get; set; } = new List<Member>(); // 複数要素の場合リスト
}

public class Member
{
    [XmlAttribute(AttributeName = "no")]
    public int No { get; set; }

    [XmlText] // ★要素の値として指定する場合この属性を使用する
    public string Value { get; set; } = "";
}

読み書きするためのヘルパー

ファイルへの読み書きは多少コードを書かないといけないので共通化して以下のクラスとします。

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Xml;
using System.Xml.Serialization;

public static class MyXmlSerializer
{
    /// <summary>
    /// 指定したオブジェクトをXML文字列に変換します。
    /// </summary>
    public static string SerializeToString<T>(T graph,
        XmlWriterSettings setting = null, XmlSerializerNamespaces ns = null)
    {
        using (var ms = new MemoryStream())
        {
            var serializer = new XmlSerializer(typeof(T));

            if (setting == null)
            {
                setting = new XmlWriterSettings() // 指定されていない場合の規定値
                {
                    Encoding = new UTF8Encoding(false), // BOMなしUTF-8
                    Indent = true,                      // インデントあり
                    IndentChars = "  ",                 // インデントはスペース2つ
                    OmitXmlDeclaration = true,          // XML宣言を除去
                };
            }
            if (ns == null)
            {
                // 指定されていない場合の規定値 = 名前空間使わない
                ns = new XmlSerializerNamespaces();
                ns.Add(string.Empty, string.Empty);
            }

            using (var xw = XmlWriter.Create(ms, setting))
            {
                serializer.Serialize(xw, graph, ns);
                return setting.Encoding.GetString(ms.ToArray());
            }
        }
    }

    /// <summary>
    /// 指定したオブジェクトをXMLとしてファイルに書き出します。
    /// </summary>
    public static void SerializeToFile<T>(string savePath, T graph)
    {
        using (var sw = new StreamWriter(savePath, false, Encoding.UTF8))
        {
            var ns = new XmlSerializerNamespaces();
            ns.Add(string.Empty, string.Empty);

            new XmlSerializer(typeof(T)).Serialize(sw, graph, ns);
        }
    }

    /// <summary>
    /// 指定した文字列をオブジェクトに復元します。
    /// </summary>
    public static T DeserializeFromString<T>(string text)
    {
        using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(text)))
        {
            return (T)new XmlSerializer(typeof(T)).Deserialize(ms);
        }
    }

    /// <summary>
    /// 指定したファイルの内容を T で指定した型に復元します。
    /// </summary>
    public static T DeserializeFromFile<T>(string loadPath)
    {
        using (var sr = new StreamReader(loadPath))
        {
            return (T)new XmlSerializer(typeof(T)).Deserialize(sr);
        }
    }
}

使い方

オブジェクト⇔ファイル間のシリアライズ・デシリアライズは以下の通りです。

// ファイルから読み取る
Root deserializedObect = MyXmlSerializer.Deserialize<Root>(@"d:\xmlser.xml");

// ファイルに書き込む
MyXmlSerializer.Serialize(@"d:\xmlser_2.xml", deserializedObect);

オブジェクト⇔文字列の変換は以下の通りです。

static void Main(string[] args)
{
    var root = new Root();
    root.SubList.Add(new Sub()
    {
        Attributte = "att1",
        StringNode = "str1",
        ValueInt = 111,
        ValueDouble = 0.123
    });
    root.SubList.Add(new Sub()
    {
        Attributte = "att1",
        StringNode = "str1",
        ValueInt = 222,
        ValueDouble = 9.876
    });
    root.MemberList.Members.Add(new Member()
    {
        No = 1,
        Value = "mmmm"
    });
    root.MemberList.Members.Add(new Member()
    {
        No = 2,
        Value = "qqqq"
    });

    // オブジェクトから文字列に変換する
    string xml = MyXmlSerializer.SerializeToString(root);
    Console.WriteLine(xml);
    // 出力:
    // <root>
    //   <sub att = "att1">
    //     <text> str1 </text>
    //     <value-i>111 </value-i>
    //     <value-d>0.123 </value-d>
    //   </sub>
    //   <sub att="att1">
    //     <text>str1</text>
    //     <value-i>222</value-i>
    //     <value-d>9.876</value-d>
    //   </sub>
    //   <list>
    //     <member no="1">mmmm</member>
    //     <member no="2">qqqq</member>
    //   </list>
    // </root>

    // 元に戻せる
    var root2 = MyXmlSerializer.DeserializeFromString<Root>(xml);
    Console.WriteLine(root2);
}

DateTime型の取り扱い

DateTime型を読み書きする場合以下のように設定すると変換できます。

// まず文字列で読み込んでしまう
[XmlElement(ElementName = "date")]
public string Date { get; set; }

// 実際に使用するのはこっち。変換する処理を書いてしまう。
[XmlIgnore] // ← シリアライズの対象から外す
public DateTime
{
    get
    {
        return DateTime.ParseExact(this.Date, "yyyyMMdd", ....);
    }
}

まとめ

クラスさえ用意すれば、ほぼ1行で読み書きできるようになります。整数や小数、bool型は自動で解釈してオブジェクトに入れてくれます。オブジェクトとXMLの順序が違っても読み込んでくれます。

また、読み込んだファイルをそのまま別のファイルへ書き出して差分を取って全て一致していればテスト確認が即座に完了する点や、読み書きのロジックを手で実装しないで済むためパースするコード大幅に削減できます。