C#でJsonをSerialize/Deserializeする方法

# 実行環境
VS2019 Version 16.4.5
.NET Framework 4.7.2

# 使用するSerializer
- DataContractJsonSerailizer
    - System.Runtime.Serializationを参照追加すれば使用可能。

NuGetで優秀なライブラリが数多く配布されてはいますが、
ここでは基本的なDataContractJsonSerializerを使用して分かりやすく説明します。

# 実装方法

まずはSerialize/Deserializeするためのシンプルなコード例を示します。

###実装コード（シンプル）
```CS:Program.cs
public class Test
{
    public string PublicStringProperty { get; set; } = "Public String Property";
    public string PublicStringField = "Public String Field";
    string PrivateStringProperty { get; set; } = "Private String Property";
    string PrivateStringField = "Private String Field";
}

class Program
{
    static void Main(string[] args)
    {
        var test = new Test();

        using (var fs = new FileStream("test.json", FileMode.Create))
        {
            var serializer = new DataContractJsonSerializer(typeof(Test));
            serializer.WriteObject(fs, test);
        }

        using (var fs = new FileStream("test.json", FileMode.Open))
        {
            var serializer = new DataContractJsonSerializer(typeof(Test));
            var dest = serializer.ReadObject(fs);
        }
    }
}
```
###出力結果
```JSON:test.json
{"PublicStringField":"Public String Field","PublicStringProperty":"Public String Property"}
```
見て分かる通り、`public`なプロパティとフィールドだけ出力されています。`private`なメンバは出力されません。

また`[IgnoreDataMember]`でマークすると、そのメンバだけSerialize/Deserializeできないようになります。

```CS:IgnroeDataMemberの例
[IgnoreDataMember] // Serialize/Deserializeされなくなる
public string PublicStringProperty { get; set; } = "Public String Property";
```

プロパティの場合、プロパティ本体が`public`指定でも、`get/set`どちらかが`public`以外(`internal/protected/private`)になるとSerialize/Deserializeできなくなります。

```CS:プロパティのアクセス指定によるSerialize/Deserialize
public class Test
{
    // Serialize/Deserializeされる
    public int Value0 { get; set; } 
    
    // Serialize/Deserializeされない
    public int Value1 { get; internal set; } 
    public int Value2 { internal get; set; }
    public int Value3 { get; protected set; } 
    public int Value4 { protected get; set; }
    public int Value5 { get; private set; } 
    public int Value6 { private get; set; }
}
```
　
さて、出力できましたが改行されていないのでjsonコードが見づらいです。
段落及びインデントで整形してSerialize/Deserializeできるようにします。

###実装コード（整形あり）
```CS
using (var fs = new FileStream("test.json", FileMode.Create))
{
    var serializer = new DataContractJsonSerializer(typeof(Test));
    var jsonWriter = JsonReaderWriterFactory.CreateJsonWriter(fs, Encoding.UTF8, true, true, "    ");
    serializer.WriteObject(jsonWriter, test);
    jsonWriter.Dispose();
}

using (var fs = new FileStream("test.json", FileMode.Open))
{                
    var serializer = new DataContractJsonSerializer(typeof(Test));
    var jsonReader = JsonReaderWriterFactory.CreateJsonReader(fs, XmlDictionaryReaderQuotas.Max);
    var dest = serializer.ReadObject(jsonReader);
    jsonReader.Dispose();
}
```

###出力結果
```JSON:test.json
{
    "PublicStringField": "Public String Field",
    "PublicStringProperty": "Public String Property"
}
```
`jsonWriter`の`Dispose`実行を忘れないよう気を付けてください。
（`Dispose`処理時に`FileStream`を経由して書き込まれるため）

`using`で書き込めば安全に出力することができます。

```CS
using (var jsonWriter = JsonReaderWriterFactory.CreateJsonWriter(fs, Encoding.UTF8, true, true, "    "))
{
    serializer.WriteObject(jsonWriter, test);
}
```
`JsonReaderWriterFactory.CreateJsonWriter`の第5引数がインデントに代わる文字です。
半角スペース4文字を設定しています。

`XmlDictionaryWriter`（`JsonReaderWriterFactory.CreateJsonWriter`で生成したWriter）経由で出力したデータは、記載したコードのように`XmlDictionaryReader`経由で読み取るようにしてください。それ以外の方法で読み込むと例外が発生します。

###DataContractAttributeについて
先程Serialize/Deserializeするクラスには何もAttributeをマークしていませんでした。
よく、`DataContractAttribute`をマークしてSerialize/Deserializeする必要がある、と見聞きしますがそんなことはありません。

`DataContract`はあくまで明示することにより、**Serialize/Deserializeに便利な挙動を提供できる仕組み**でしかありません。（Serialize/Deserializeするプロパティ名を変えたり、Serialize/Deserializeの順番を変えたり等）

ここでは、`DataContract`及び`DataMember`をマークした場合の挙動について説明します。

###実装コード(DataContractを使用した場合)
```CS:DataContractを使用した例
[DataContract]
public class Test
{
    [DataMember]
    public string PublicStringProperty { get; set; } = "Public String Property";

    public string PublicStringField = "Public String Field";

    string PrivateStringProperty { get; set; } = "Private String Property";

    [DataMember]
    string PrivateStringField = "Private String Field";
}
```
###出力結果
```JSON:test.json
{
    "PrivateStringField": "Private String Field",
    "PublicStringProperty": "Public String Property"
}
```
`[DataContract]`でマークしたクラスは、`[DataMember]`でマークしたメンバのみSerialize/Deserializeされます。`private`であっても、Serialize/Deserializeすることが可能です。

その他、**引数付きコンストラクタのみを実装したクラス**でもSerialize/Deserialize可能になります。

`[DataContract]`をマークしていない場合、
**引数付きコンストラクタのみ実装されたクラス**はSerialize/Deserializeできません。
（デフォルトコンストラクタを実装すればSerialize/Deserializeできます）

```CS
// Serialize/Deserialize可能
[DataContract]
public class Test
{
    public Test(int value){}
}

// Serialize/Deserializeで例外が発生する
public class Test
{
    public Test(int value){}
}

// Serialize/Deserialize可能
public class Test
{
    public Test(){}
    public Test(int value){}
}
```

###DataContractの注意点
`[DataContract]`でマークすると、引数付きコンストラクタのみでもSerialize/Deserialize可能でした。
逆に言えば、**デフォルトコンストラクタが動作していない**ということが説明できます。

これは`[DataContract]`でマークしたクラスは、Deserialize時に対象クラスを`FormatterServices.GetUninitializedObject(Type)`を使用してオブジェクト生成されるからです。

```CS
[DataContract]
public class Test
{
	[DataMember]
	public string PublicStringProperty { get; set; } = "この文字で初期化されない";
}
```

Deserialize時に、ある特定の値に初期化しておきたい場合は、`[OnDeserializing]`をマークしたメソッドを実装します。

```CS
[DataContract]
public class Test
{
	[DataMember]
	public string PublicStringProperty { get; set; };
	
    [OnDeserializing]
    void DefaultDeserializing(StreamingContext sc)
    {
    	PublicStringProperty = "Public String Property";
    }
}
```

基本的にコード上でクラス生成されるので、デフォルトコンストラクタが動作して初期化されるとは思います。
しかし、後述する未初期化ケースによって問題になることがあります。（ありました）

###問題パターン
`Test.Value`は、エディター上で3の値に編集されて、`test.json`として出力されたとします。

```CS:出力する型
[DataContract]
public class Test
{
    [DataMember]
    public int Value; // 3が入る
}
```

```JSON:test.json
{
    "Value": 3
}
```

仕様変更が入り、`Test.Value`→`Test.Value2`と名前が変更されたとします。
役割が同じということだったので、初期値は3が入るようにコードを修正します。

```CS:仕様変更後の型
[DataContract]
public class Test
{
    [DataMember]
    public int Value2 = 3;
}
```
この状態で`test.json`をDeserializeするとどうなるでしょう。
`test.json`には、`Value`というフィールドが存在しないため、`Value2`の読み込みは行われません。

そしてデフォルトコンストラクタも動作しないので、
この`Value2`は3で初期化されず、**intの規定値となる0で初期化**されます。

仕様変更後の型で仕様変更前のデータをDeserializeする際、こういった不具合の発生原因を作ってしまいます。

この場合、対応方法は各種ありますが、DataContractを使用して仕様変更前のデータをDeserializeしたいのであれば`DataMember(Name = "")` で以前のフィールド名（プロパティ名）指定してあげれば、Deserializeすることが可能です。

```CS:仕様変更前のtest.jsonをDeserializeするためのコード
[DataContract]
public class Test
{
    [DataMember(Name = "Value")]
    public int Value2;
}
```
このような部分が、DataContractのメリットでもあり、デメリットでもあります。

###Serializeパターン
では、下記のようなパターンはSerialize/Deserializeできるでしょうか。

```CS
public class Child
{
    public int Value = 0;
}

[DataContract]
public class Parent
{
    [DataMember]
    public Child ChildValue = new Child();
}
```

```JSON:parent.json
{
    "ChildValue": {
        "Value": 0
    }
}
```
Rootとなる型だけ、`DataContract`をマークしておけばSerialize/Deserialize可能です。
　
では、継承を用いた少し複雑なパターンはどうでしょうか。
(説明に不要なコードは、使用しないように修正しています）

```CS:Program.cs
[DataContract]
public class Super
{
    [DataMember]
    public int SuperValue = 0;
}

[DataContract]
public class Sub : Super
{
    [DataMember]
    public int SubValue = 1;
}

class Program
{
    static void Main(string[] args)
    {
        Sub instance = new Sub();

        using (var fs = new FileStream("sub.json", FileMode.Create))
        {
            var serializer = new DataContractJsonSerializer(typeof(Super));
            serializer.WriteObject(fs, instance);
        }

        using (var fs = new FileStream("sub.json", FileMode.Open))
        {
            var serializer = new DataContractJsonSerializer(typeof(Super));
            var dest = serializer.ReadObject(fs);
        }
    }
}
```

残念ながら、例外が発生します。(`System.Runtime.Serialization.SerializationException`)
基底クラス（`Super`）としてSerializeするはずが、型を知らない派生クラス（`Sub`）でSerializeしているからです。もちろん、`Sub intance`の宣言を`Super instance`にしても、例外は発生します。

派生クラスの型さえ伝えられれば、たとえ基底クラスの型を指定していてもSerializeが可能になります。
（当たり前ですが、継承関係にあることが前提です）

```CS:DataContractJsonSerializerSettingsを使用する例
Sub instance = new Sub();

var settings = new DataContractJsonSerializerSettings()
{
    KnownTypes = new Type[] { typeof(Sub) }
};

using (var fs = new FileStream("sub.json", FileMode.Create))
{
    var serializer = new DataContractJsonSerializer(typeof(Super), settings);
    serializer.WriteObject(fs, instance);
}

using (var fs = new FileStream("sub.json", FileMode.Open))
{
    var serializer = new DataContractJsonSerializer(typeof(Super), settings);
    var dest = serializer.ReadObject(fs);
}
```

`DataContractJsonSerializerSettings.KnownTypes`に、継承関係にある`Sub`のTypeを配列で入れます。
これにより、`DataContractJsonSerializer`に基底クラスの型を指定しても、継承されたクラスのインスタンスをSerialize/Deserializeすることが可能になります。

###DataContractJsonSerializerSettingsについて
`KnownTypes`以外にも設定できる項目があります。
`DataContractJsonSerializerSettings.EmitTypeInformation`に`EmitTypeInformation.Always`を指定すれば、型情報も出力されるようになります。

```CS:Program.cs
namespace CSharp_sandbox
{
    [DataContract]
    public class Test
    {
        [DataMember]
        public int Value = 1;
    }

    class Program
    {
        static void Main(string[] args)
        {
            var test = new Test();

            var settings = new DataContractJsonSerializerSettings()
            {
                EmitTypeInformation = EmitTypeInformation.Always
            };

            using (var fs = new FileStream("test.json", FileMode.Create))
            {
                using (var writer = JsonReaderWriterFactory.CreateJsonWriter(fs, Encoding.UTF8, true, true, "    "))
                {
                    var serializer = new DataContractJsonSerializer(typeof(Test), settings);
                    serializer.WriteObject(writer, test);
                }
            }



            using (var fs = new FileStream("test.json", FileMode.Open))
            {
                using (var reader = JsonReaderWriterFactory.CreateJsonReader(fs, XmlDictionaryReaderQuotas.Max))
                {
                    var serializer = new DataContractJsonSerializer(typeof(Test), settings);
                    var dest = serializer.ReadObject(reader);
                }
            }
        }
    }
}
```
```JSON:test.json
{
    "__type": "Test:#CSharp_sandbox",
    "Value": 1
}
```
`型名`:#`名前空間`という形で出力されます。

`DataContractJsonSerializerSettings.UseSimpleDictionaryFormat`を`true`にすると、
`Dictionary<TKey,TValue>`をSerializeする時のデータ形式が変わります。

```CS
var settings = new DataContractJsonSerializerSettings()
{
    UseSimpleDictionaryFormat = true
};
```

trueの場合

```JSON:UseSimpleDictionaryFormat=true
{
    "Dict": {
        "TestKey0": 0,
        "TestKey1": 1,
        "TestKey2": 2
    }
}
```

falseの場合(規定値）

```JSON:UseSimpleDictionaryFormat=false
{
    "Dict": [
        {
            "Key": "TestKey0",
            "Value": 0
        },
        {
            "Key": "TestKey1",
            "Value": 1
        },
        {
            "Key": "TestKey2",
            "Value": 2
        }
    ]
}
```

名前の通り、`Key:Value`という形でシンプルにSerializeされるようになります。

#終わりに
自分でもたまに`DataContract`について混乱することがあったため、検証がてら記事にしてみました。
この記事が誰かの助けになれば幸いです。

間違い等があれば、ご指摘していただけると助かります。