DataContractJsonSerializerの詳細動作

以前、.NET標準のDataContractJsonSerializerの利用について少し書きました。

mokake.hatenablog.com

このクラス、アプリケーション設定の保存や、ある種のデータの保存にも便利ですが、本格的に使おうとすると、詳細な動作が気になります。

  • データを格納するクラスにDataContract属性、プロパティにDataMember属性をつけるとあるが、つけないとどうなるのか
  • プロパティのsetterがprivateである場合、明示的なprivateフィールドと結びついている場合はどうなのか
  • Json化したくない項目にIgnoreDataMember属性をつけるが、これはDataContract属性の影響を受けるのか
  • プロパティの順序は意味があるのか
  • プロパティが違うデータを読み込んだ場合はどうなるのか(データクラスの更新によって項目の増減があった場合を想定しています)
  • POCOなどシンプルな形式のクラスは処理できるか、また派生クラスはどうなのか
  • デフォルトコンストラクタとJSONからのデシリアライズ結果の関係はどうなるのか。またprivateフィールド初期値との関係はどうか。

こういったところを、実際の動作から確認してみたいと思います。

改めて、参照について

DataContractJsonSerializerを使うためには、.NET3.5の場合、プロジェクトの参照に次の2つを加える必要があります。

  • System.Runtime.Serialization
  • System.ServiceModel.Web

特に後者が分かりづらく困るところです。

.NET4以降では、後者は不要となりました。

  • System.Runtime.Serialization

だけでOKです。

結果

途中は全部省いて、結果のみを箇条書きにします。なお使ったコードの主要部分は、このあとに掲載しています。

  • DataContractDataMemberがなくても、publicなgetter, setterがあればシリアライズもデシリアライズも可能。private項目はシリアライズされず、デシリアライズの際はprivateフィールドよりもデフォルトコンストラクタの結果が優先される(つまり、普通にデフォルトコンストラクタが実行された後でpublic setterが呼ばれる)。
  • DataContractをクラスにつけると、DataMemberをつけた項目だけがシリアライズの対象となる(publicでも属性なしならJSON化はされない)。またデシリアライズの際はフィールド初期値やコンストラクタは適用されない。
  • DataContractなしの場合、DataMember属性は特に意味をもたない。エラーも出ないがJSON関連の動作は何もないのと同じ。
  • DataContractDataMemberの両方をつけると、setterがprivateでもあっても無視してJSONに基づき設定される。
  • DataContractDataMemberがないクラス・プロパティでも、IgnoreDataMemberは有効で、つけておくとシリアライズ、デシリアライズ共に除外される。
  • プロパティの出現順は影響しない。シリアライズの際、プロパティはプロパティ名の文字コード順に出力される。
  • プロパティを追加または削除した場合(ただしDataContractDataMemberは設定済みとする)、前のデータを読み込んでも壊れることはない。
    • 追加項目は、初期状態で生成される(フィールド初期値やコンストラクタが反映されないので注意)。
    • 削除項目は、単に無視される。特に例外などは出ない。
  • POCOなクラスのインスタンスは、普通にシリアライズもデシリアライズも可能。Listでも大丈夫。
  • 基底クラスのプロパティに派生クラスを代入した場合、シリアライズできない(SerializationExceptionが発生する)。

ちなみに、Ubuntu16.04上のMonoで実行しても、これらは同じでした。

まとめ

結果を簡単にまとめます。

  • DataContractJsonSerializerの対象データクラスは、DataContractをつけるかどうかで挙動が変わる。
    • つけると、privateも設定できるがDataMember属性必須となる。
    • つけないと、デフォルトコンストラクタなどが普通に動くが、publicなsetterがないと対象外となる。
  • IgnoreDataMemberだけはいつでも有効。
  • 項目が変化した場合の動作は「ないものは初期状態」。
  • 派生クラスを基底クラスに設定していると例外。

参考1 : JSON化と復元

今回はシンプルなstaticメソッドにしています。また、例外発生時はそのまま再スローしています。

なお、今回の.NETのバージョンは4.5です(でも3.5でも参照などを除けば同様に動作するはず)。

using System;
using System.Text;
using System.IO;
using System.Runtime.Serialization.Json;

namespace JsonTestNET45
{
    public static class Json
    {
        public static string ToJson<T>(this T data)
        {
            string json = null;
            var stream = new MemoryStream();
            try
            {
                var serializer = new DataContractJsonSerializer(typeof(T));
                serializer.WriteObject(stream, data);
                json = Encoding.UTF8.GetString(stream.ToArray());
            }
            catch (Exception ex)
            {
                throw;
            }
            finally
            {
                stream.Dispose();
            }
            return json;
        }

        public static T ToObject<T>(this string json)
        {
            var stream = new MemoryStream(Encoding.UTF8.GetBytes(json));
            T data = default(T);
            try
            {
                var serializer = new DataContractJsonSerializer(typeof(T));
                data = (T)serializer.ReadObject(stream);
            }
            catch (Exception ex)
            {
                throw;
            }
            finally
            {
                stream.Dispose();
            }
            return data;
        }
    }
}

参考2 : データクラス

色々なケースを考えて、多数のデータクラスを作ってみました。ToStringのオーバーライドの書式などは.NET4.5の機能を使っています。

using System.Collections.Generic;
using System.Runtime.Serialization;
using System.Text;

namespace JsonTestNET45
{
    public interface SampleAvailable
    {
        void SetSample();
    }

    public class Model1_NoAttribute : SampleAvailable
    {
        public string Text1 { get; set; }

        public string Text2 { get; private set; }

        public string Text3
        {
            get { return _text3; }
            set { _text3 = value; }
        }
        private string _text3 = "text3-default";

        public string Text4
        {
            get { return _text4; }
            private set { _text4 = value; }
        }
        private string _text4 = "text4-default";

        public Model1_NoAttribute()
        {
            Text1 = "constructor1";
            Text2 = "constructor2";
            Text3 = "constructor3";
            Text4 = "constructor4";
        }

        public void SetSample()
        {
            Text1 = "Model1-text1";
            Text2 = "M1-text2";
            Text3 = "M1-text3";
            Text4 = "M1-text4";
        }

        public override string ToString()
        {
            return $"{GetType().Name} : Text1=[{Text1}] Text2=[{Text2}] Text3=[{Text3}] Text4=[{Text4}]";
        }
    }

    [DataContract]
    public class Model2_DataContract : SampleAvailable
    {
        public string Text1 { get; set; }

        public string Text2 { get; private set; }

        public string Text3
        {
            get { return _text3; }
            set { _text3 = value; }
        }
        private string _text3 = "text3-default";

        public string Text4
        {
            get { return _text4; }
            private set { _text4 = value; }
        }
        private string _text4 = "text4-default";

        public Model2_DataContract()
        {
            Text1 = "constructor1";
            Text2 = "constructor2";
            Text3 = "constructor3";
            Text4 = "constructor4";
        }

        public void SetSample()
        {
            Text1 = "Model2-text1";
            Text2 = "M2-text2";
            Text3 = "M2-text3";
            Text4 = "M2-text4";
        }

        public override string ToString()
        {
            return $"{GetType().Name} : Text1=[{Text1}] Text2=[{Text2}] Text3=[{Text3}] Text4=[{Text4}]";
        }
    }

    public class Model3_DataMember : SampleAvailable
    {
        [DataMember]
        public string Text1 { get; set; }

        [DataMember]
        public string Text2 { get; private set; }

        [DataMember]
        public string Text3
        {
            get { return _text3; }
            set { _text3 = value; }
        }
        private string _text3 = "text3-default";

        [DataMember]
        public string Text4
        {
            get { return _text4; }
            private set { _text4 = value; }
        }
        private string _text4 = "text4-default";

        public Model3_DataMember()
        {
            Text1 = "constructor1";
            Text2 = "constructor2";
            Text3 = "constructor3";
            Text4 = "constructor4";
        }

        public void SetSample()
        {
            Text1 = "Model3-text1";
            Text2 = "M3-text2";
            Text3 = "M3-text3";
            Text4 = "M3-text4";
        }

        public override string ToString()
        {
            return $"{GetType().Name} : Text1=[{Text1}] Text2=[{Text2}] Text3=[{Text3}] Text4=[{Text4}]";
        }
    }

    [DataContract]
    public class Model4_Both : SampleAvailable
    {
        [DataMember]
        public string Text1 { get; set; }

        [DataMember]
        public string Text2 { get; private set; }

        [DataMember]
        public string Text3
        {
            get { return _text3; }
            set { _text3 = value; }
        }
        private string _text3 = "text3-default";

        [DataMember]
        public string Text4
        {
            get { return _text4; }
            private set { _text4 = value; }
        }
        private string _text4 = "text4-default";

        public Model4_Both()
        {
            Text1 = "constructor1";
            Text2 = "constructor2";
            Text3 = "constructor3";
            Text4 = "constructor4";
        }

        public void SetSample()
        {
            Text1 = "Model4-text1";
            Text2 = "M4-text2";
            Text3 = "M4-text3";
            Text4 = "M4-text4";
        }

        public override string ToString()
        {
            return $"{GetType().Name} : Text1=[{Text1}] Text2=[{Text2}] Text3=[{Text3}] Text4=[{Text4}]";
        }
    }

    public class Model5_IgnoreDataMember : SampleAvailable
    {
        [IgnoreDataMember]
        public string Text1 { get; set; }

        public string Text2 { get; private set; }

        public string Text3
        {
            get { return _text3; }
            set { _text3 = value; }
        }
        private string _text3 = "text3-default";

        public string Text4
        {
            get { return _text4; }
            private set { _text4 = value; }
        }
        private string _text4 = "text4-default";

        public Model5_IgnoreDataMember()
        {
            Text1 = "constructor1";
            Text2 = "constructor2";
            Text3 = "constructor3";
            Text4 = "constructor4";
        }

        public void SetSample()
        {
            Text1 = "Model5-text1";
            Text2 = "M5-text2";
            Text3 = "M5-text3";
            Text4 = "M5-text4";
        }

        public override string ToString()
        {
            return $"{GetType().Name} : Text1=[{Text1}] Text2=[{Text2}] Text3=[{Text3}] Text4=[{Text4}]";
        }
    }

    [DataContract]
    public class Model6_Order : SampleAvailable
    {
        [DataMember]
        public string Text4
        {
            get { return _text4; }
            private set { _text4 = value; }
        }
        private string _text4 = "text4-default";

        [DataMember]
        public string Text2 { get; private set; }

        [DataMember]
        public string Text1 { get; set; }

        [DataMember]
        public string Text3
        {
            get { return _text3; }
            set { _text3 = value; }
        }
        private string _text3 = "text3-default";

        public Model6_Order()
        {
            Text1 = "constructor1";
            Text2 = "constructor2";
            Text3 = "constructor3";
            Text4 = "constructor4";
        }

        public void SetSample()
        {
            Text1 = "Model6-text1";
            Text2 = "M6-text2";
            Text3 = "M6-text3";
            Text4 = "M6-text4";
        }

        public override string ToString()
        {
            return $"{GetType().Name} : Text1=[{Text1}] Text2=[{Text2}] Text3=[{Text3}] Text4=[{Text4}]";
        }
    }

    [DataContract]
    public class Model7_Changed : SampleAvailable
    {
        [DataMember]
        public string Text5 { get; set; }

        [DataMember]
        public string Text2 { get; private set; }

        [DataMember]
        public string Text3
        {
            get { return _text3; }
            set { _text3 = value; }
        }
        private string _text3 = "text3-default";

        [DataMember]
        public string Text4
        {
            get { return _text4; }
            private set { _text4 = value; }
        }
        private string _text4 = "text4-default";

        public Model7_Changed()
        {
            Text2 = "constructor2";
            Text3 = "constructor3";
            Text4 = "constructor4";
            Text5 = "constructor5";
        }

        public void SetSample()
        {
            Text2 = "Model3-text2";
            Text3 = "M3-text3";
            Text4 = "M3-text4";
            Text5 = "M3-text5";
        }

        public override string ToString()
        {
            return $"{GetType().Name} : Text2=[{Text2}] Text3=[{Text3}] Text4=[{Text4}] Text=[{Text5}]";
        }
    }

    [DataContract]
    public class Base
    {
        [DataMember]
        public string Text1 { get; set; }

        public override string ToString()
        {
            return $"{GetType().Name} : Text1=[{Text1}]";
        }
    }

    [DataContract]
    public class Derived1 : Base
    {
        [DataMember]
        public string Text2 { get; set; }

        public override string ToString()
        {
            return $"{GetType().Name} : Text1=[{Text1}] Text2=[{Text2}]";
        }
    }

    public class Model8_Base : SampleAvailable
    {
        public List<Base> Items { get; set; }

        public Model8_Base()
        {
            Items = new List<Base> {
                new Base { Text1 = "constructor base" },
                new Base { Text1 = "constructor 2" },
            };
        }

        public void SetSample()
        {
            Items = new List<Base> {
                new Base { Text1 = "Base-Item" },
                new Base { Text1 = "2nd base item" },
            };
        }

        public override string ToString()
        {
            var sb = new StringBuilder($"{GetType().Name} :");
            if (Items != null)
            {
                foreach (var item in Items)
                {
                    sb.Append($" <{item}>");
                }
            }
            return sb.ToString();
        }
    }

    public class Model9_Derive : SampleAvailable
    {
        public List<Base> Items { get; set; }

        public Model9_Derive()
        {
            Items = new List<Base> {
                new Base { Text1 = "constructor base" },
                new Derived1 { Text1 = "constructor derived1-1", Text2 = null },
                new Derived1 { Text1 = "constructor derived1-2", Text2 = "Extended" },
            };
        }

        public void SetSample()
        {
            Items = new List<Base> {
                new Base { Text1 = "Base-Item" },
                new Derived1 { Text1 = "Derived1-Item1", Text2 = null },
                new Derived1 { Text1 = "Derived1-Item2", Text2 = "Extended" },
            };
        }

        public override string ToString()
        {
            var sb = new StringBuilder($"{GetType().Name} :");
            if (Items != null)
            {
                foreach (var item in Items)
                {
                    sb.Append($" <{item}>");
                }
            }
            return sb.ToString();
        }
    }
}