seraphyの日記

日記というよりは過去を振り返るときのための単なる備忘録

JavaのJAXBに相当することをC#で行う方法

C#ではXmlSerializerがJavaのJAXBに相当する

前回は、JavaXMLをあつかう方法に関連してJAXBを調べてみたので、今回は、それと同等なことをC#で試してみる。


DotNET FrameworkはVer1.1のころからXMLには非常に手厚いサポートがあり、
JavaのJAXBに相当するものはXmlSerializerとして昔から存在している。

Java Architecture for XML Binding(JAXB)は、Javaのクラスを XMLで表現可能にする仕様である。JAXB には主に2つの機能がある。すなわち、Java のオブジェクトを XMLシリアライズすることと、逆に XML から Java オブジェクトにデシリアライズすることである。言い換えれば、JAXB はメモリ上のデータを XML 形式に変換して保存することができ、そのためにプログラム内の各クラスにXMLロード/セーブルーチンを実装する必要がない。

http://ja.wikipedia.org/wiki/Java_Architecture_for_XML_Binding より引用

XmlSerializerの使用例 (C#4.0)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Serialization;
using System.IO;
using System.Xml.Schema;
using System.Xml;

namespace XmlSerializeTest
{
    /// <summary>
    /// ルート要素
    /// </summary>
    [XmlRoot(ElementName = "bookCatalog")] // ルート要素の明示
    public class BookCatalog
    {
        [XmlElement(ElementName = "comment")] // 要素の明示
        public string Comment { set; get; }

        private List<Book> books = new List<Book>();

        [XmlArray(ElementName = "books")] // リストを囲む外側の要素を指定
        [XmlArrayItem(
            ElementName = "book", // リストの要素を指定
            Type = typeof(Book))] // リストの要素の型を明示 (型が明らかであれば省略可)
        public List<Book> Books
        {
            get
            {
                return books;
            }
        }

        public override string ToString()
        {
            var buf = new StringBuilder();
            buf.Append("(Comment=").Append(Comment);
            buf.Append(", Books=[").Append(string.Join(", ", books)).Append("]");
            buf.Append(")");
            return buf.ToString();
        }
    }

    /// <summary>
    /// Book情報
    /// </summary>
    public class Book
    {
        [XmlElement(ElementName = "title", IsNullable = false)]
        public string Title { set; get; }

        protected List<Author> authors = new List<Author>();

        [XmlElement(
            ElementName = "author", // 外側の指定なし、booksの場合と出力の違いに注意
            Type = typeof(Author))] // 要素の型を明示することが可能 (型が明らかであれば省略可)
        public List<Author> Authors
        {
            get
            {
                return authors;
            }
        }

        // [XmlElement(ElementName = "price")] (属性の指定なくても暗黙で出力される)
        public decimal Price { set; get; }

        [XmlElement(ElementName = "published", DataType = "date")] // データのXML型を明示する.
        public DateTime Published { set; get; }

        public override string ToString()
        {
            var buf = new StringBuilder();
            buf.Append("(Title=").Append(Title);
            buf.Append(", Authors=[").Append(string.Join(", ", authors)).Append("]");
            buf.Append(", Price=").Append(Price);
            buf.Append(", Published=").Append(Published);
            buf.Append(")");
            return buf.ToString();
        }
    }

    /// <summary>
    /// Author情報
    /// </summary>
    public class Author
    {
        [XmlAttribute] // 名前の指定を省略するとプロパティ名がそのまま使われる.
        public string AuthorId { set; get; }
        
        [XmlText] // 要素のコンテンツを値とする場合 (xsdでいうところのsimpleContent)
        public string Name { set; get; }

        public override string ToString()
        {
            var buf = new StringBuilder();
            buf.Append("(AuthorId=").Append(AuthorId);
            buf.Append(", Name=").Append(Name);
            buf.Append(")");
            return buf.ToString();
        }
    }

    class Program
    {
        /// <summary>
        /// エントリポイント
        /// </summary>
        static void Main(string[] args)
        {
            // テストデータの生成
            var books = makeBookCatalog();

            // XMLにシリアライズする.
            var serializer = new XmlSerializer(typeof(BookCatalog));
            string xml;
            using (TextWriter txwr = new StringWriter())
            {
                serializer.Serialize(txwr, books);
                xml = txwr.ToString();
            }
            System.Diagnostics.Debug.WriteLine("xml=" + xml);

            // スキーマの取得
            // (EXEファイルに埋め込みリソースとして入れたxsdファイル)
            XmlSchemaSet schemas = new XmlSchemaSet();
            using (var stm = typeof(Program).Assembly
                .GetManifestResourceStream("XmlSerializeTest.schema.xsd"))
            {
                schemas.Add("", XmlReader.Create(stm));
            }

            // XMLファイルの読み込み時にスキーマによる検証を行うように設定
            var settings = new XmlReaderSettings();
            settings.ValidationType = ValidationType.Schema;
            settings.Schemas.Add(schemas);
            settings.Schemas.Compile();

            // XMLファイルからオブジェクトをデシリアライズする.
            BookCatalog books2;
            using (var txrd = new StringReader(xml))
            using (var xmlReader = XmlReader.Create(txrd, settings))
            {
                books2 = (BookCatalog)serializer.Deserialize(xmlReader);
            }
            System.Diagnostics.Debug.WriteLine("books2=" + books2);
        }

        /// <summary>
        /// テストデータの生成
        /// </summary>
        /// <returns>テストデータ</returns>
        private static BookCatalog makeBookCatalog()
        {
            var bookCatalog = new BookCatalog();
            foreach (int idx in Enumerable.Range(1, 3))
            {
                var book = new Book();
                book.Title = "たいとる" + idx;

                foreach (int idx2 in Enumerable.Range(1, 3))
                {
                    var author = new Author();
                    author.AuthorId = "author" + idx2;
                    author.Name = "名前" + idx2;
                    book.Authors.Add(author);
                }
                book.Price = 1000 + idx * 100;
                book.Published = DateTime.Today.AddDays(idx);
                bookCatalog.Books.Add(book);
            }
            bookCatalog.Comment = "コメントいろいろ";
            return bookCatalog;
        }
    }
}
ポイント
  • XmlRoot属性でルート要素を明示する.
  • XmlElement属性で要素を明示する.
    • DataTypeでXML Schema上のデータタイプを明示することができる。(XMLSchema生成時にも反映される)
  • XmlArray属性, XmlArrayItem属性で、要素のリストを明示する
    • XmlElement属性でもリスト化できるが、XmlArrayで外側の要素を指定することができる.
  • XmlAttribute属性でアトリビュートを明示する.
  • XmlText属性で、要素のコンテンツを値とすることを明示できる.(XML SchemaでいうところのsimpleContent)
  • XmlElement, XmlAttributeの指定がなくても明示的にXmlIgnoreしていないフィールドはシリアライズされる.

XSDツール (Microsoft Xml Schemas/DataTypes support utility)

DotNET Framework SDKには、xsd.exeというツールが付属している。

  • XMLファイルからドキュメント構造を分析してXML Schema(*.xsd)を生成できる.
  • XSDファイルから、クラスファイルを生成できる.
  • クラスファイルの構造から、XSDファイルを生成できる.
  • (その他、ADO.NET関連のデータタイプの生成機能もあり)

とくに、XMLからXSDを生成する機能は、とても重宝する

生成されるXSDは、XML文書から推定されるものであるため、要素の出現回数や必須などの制約は必ずしも正しいとは限らないが、基本的なXSDの形は生成されるので、必要に応じて出現回数などの指定を加筆修正してゆけば、お手軽にXML Schemaを完成させることができる。

(Javaと両刀使いの人ならば、Javaには欠けているXML->XSD変換ツールとして便利に使えることと思う。)


今回は、コンパイルされたEXEのアセンブリを指定し、シリアライズ対象となるルートクラスからのクラス構造で、XSDファイルを生成させてみる。

コンパイルされたEXEファイルのクラスからXSDファイルを生成する例
C:\Projects\XmlSerializeTest> xsd XmlSerializeTest\bin\Debug\XmlSerializeTest.exe /type:XmlSerializeTest.BookCatalog
Microsoft (R) Xml Schemas/DataTypes support utility
[Microsoft (R) .NET Framework, Version 4.0.30319.1]
Copyright (C) Microsoft Corporation. All rights reserved.
ファイル 'C:\Projects\XmlSerializeTest\schema0.xsd' に書き込んでいます。
得られたXSD
<?xml version="1.0" encoding="utf-8"?>
<xs:schema elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:element name="bookCatalog" nillable="true" type="BookCatalog" />
  <xs:complexType name="BookCatalog">
    <xs:sequence>
      <xs:element minOccurs="0" maxOccurs="1" name="comment" type="xs:string" />
      <xs:element minOccurs="0" maxOccurs="1" name="books" type="ArrayOfBook" />
    </xs:sequence>
  </xs:complexType>
  <xs:complexType name="ArrayOfBook">
    <xs:sequence>
      <xs:element minOccurs="0" maxOccurs="unbounded" name="book" nillable="true" type="Book" />
    </xs:sequence>
  </xs:complexType>
  <xs:complexType name="Book">
    <xs:sequence>
      <xs:element minOccurs="0" maxOccurs="1" name="title" type="xs:string" />
      <xs:element minOccurs="0" maxOccurs="unbounded" name="author" type="Author" />
      <xs:element minOccurs="1" maxOccurs="1" name="Price" type="xs:decimal" />
      <xs:element minOccurs="1" maxOccurs="1" name="published" type="xs:date" />
    </xs:sequence>
  </xs:complexType>
  <xs:complexType name="Author">
    <xs:simpleContent>
      <xs:extension base="xs:string">
        <xs:attribute name="AuthorId" type="xs:string" />
      </xs:extension>
    </xs:simpleContent>
  </xs:complexType>
</xs:schema>

ソースコード上で属性を指定しているので、生成されるスキーマは、かなりしっかりしたものが出来上がる。

たとえば、decimalやDateTime等の値型を指定している要素は暗黙でnull不可であるため、minOccurs="1"と指定されている。
(値型をNullable型にすると、minOccurs="0"にすることができる。)

結論

実行結果

上記プログラムを実行すると、以下の結果が得られる。

xml=<?xml version="1.0" encoding="utf-16"?>
<bookCatalog xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <comment>コメントいろいろ</comment>
  <books>
    <book>
      <title>たいとる1</title>
      <author AuthorId="author1">名前1</author>
      <author AuthorId="author2">名前2</author>
      <author AuthorId="author3">名前3</author>
      <Price>1100</Price>
      <published>2012-07-04</published>
    </book>
    <book>
      <title>たいとる2</title>
      <author AuthorId="author1">名前1</author>
      <author AuthorId="author2">名前2</author>
      <author AuthorId="author3">名前3</author>
      <Price>1200</Price>
      <published>2012-07-05</published>
    </book>
    <book>
      <title>たいとる3</title>
      <author AuthorId="author1">名前1</author>
      <author AuthorId="author2">名前2</author>
      <author AuthorId="author3">名前3</author>
      <Price>1300</Price>
      <published>2012-07-06</published>
    </book>
  </books>
</bookCatalog>
books2=(Comment=コメントいろいろ, Books=[(Title=たいとる1, Authors=[(AuthorId=author1, Name=名前1), (AuthorId=author2, Name=名前2), (AuthorId=author3, Name=名前3)], Price=1100, Published=2012/07/04 0:00:00), (Title=たいとる2, Authors=[(AuthorId=author1, Name=名前1), (AuthorId=author2, Name=名前2), (AuthorId=author3, Name=名前3)], Price=1200, Published=2012/07/05 0:00:00), (Title=たいとる3, Authors=[(AuthorId=author1, Name=名前1), (AuthorId=author2, Name=名前2), (AuthorId=author3, Name=名前3)], Price=1300, Published=2012/07/06 0:00:00)])

JavaのJAXBと異なり、XMLシリアライズしたいフィールドは暗黙で属性なしでもシリアライズ可能となっていて、シリアライズしたくないフィールドに対してXmlIgnore属性をつける、という指定方法となっている。

それ以外の使い方はJavaアノテーションによるJAXBの指定方法と非常によく似ている。


日付型(DateTime)もC#4.0で試した限りでは、DataTypeで指定されたXML形式に変換されているため、XML準拠のデータとして使うのであれば、そのまま使えそうである。
この点はJAXBよりも簡単のように思えた。

(※ XML準拠でないたとえば「yyyyMMdd」のような8桁整数に変換するなどの場合は、DateTimeフィールドをXmlIgnoreでシリアライズ対象外とし、かわりに「yyyyMMdd」の文字列のアクセッサを噛ませたXmlSerialize用のプロパティを設置するのが常套手段のようである。)


以上、メモ終了。