C#の JSON Serialize Tips -

今日コードを読んでいるときに、理解できない部分があったので調べてみた。今日は２つのトピックで、JSON のシリアライズについての Tips です。

# DataContract シリアライズする属性を選択する

`[DataContract]` というアトリビュートが使われていた。このアトリビュートをつけて、`[DataMember]`アトリビュートをプロパティにつけるとそれらがシリアライズ対象になる。つまり、つけないとシリアライズされない。

```csharp
public class Context
    {
        public string ActivityId { get; set; }
    }
    [DataContract]
    public class Message
    {
        public Message(int id, string name)
        {
            Id = id;
            Name = name;
        }
        [DataMember]
        public int Id { get; private set; }
        [DataMember]
        public string Name { get; private set; }

        public int Age { get; set; }
        [DataMember]
        public Context Context { get; set; }
        [DataMember]
        public IList Books { get; set; }

    }
```

こちらの公式リファレンスはこちらなのだが、`JSON` に対する記述がなくて混乱した。

* [データ コントラクトの使用](https://docs.microsoft.com/ja-jp/dotnet/framework/wcf/feature-details/using-data-contracts)

こちらを見ると、XMLのシリアライズしか書いていないので混乱したが、`NewtonJson`のサンプルのページには記述があった。

* [DataContract and DataMember Attributes](https://www.newtonsoft.com/json/help/html/DataContractAndDataMember.htm)

上記のクラスを実際にシリアライズしてみると、`[DataMember]` をつけていない Age が無視されているのがわかる。また、カスタムクラスの Context のクラス定義側には、`[DataMember]`をつけていないけど、使われている側で指定しておけばシリアライズしてくれる様子だ。

```json
{"Id":1,"Name":"ushio","Context":{"ActivityId":"abc"},"Books":["foo","bar"]}
```

# TypeNameHandler

私が呼んでいたコードでは、シリアライズ、デシリアライズする部分で、TypeNameHandler が使われていた。

* [TypeNameHandling setting](https://www.newtonsoft.com/json/help/html/SerializeTypeNameHandling.htm)
* [TypeNameHandling Enumeration](https://www.newtonsoft.com/json/help/html/T_Newtonsoft_Json_TypeNameHandling.htm)

次のような記述をすると、シリアライズ・デシリアライズの時の振る舞いを設定できる。

```csharp
            var message = new Message(1, "ushio");
            message.Age = 47;
            message.Context = new Context()
            {
                ActivityId = "abc"
            };
            message.Books = new List<string>()
            {
                "foo", "bar"
            };

            var messageJsonWithObjects = JsonConvert.SerializeObject(message, new JsonSerializerSettings()
            {
                TypeNameHandling = TypeNameHandling.Objects
            });
            Console.WriteLine("---- Object");
            Console.WriteLine(messageJsonWithObjects);
```

## 実行結果

`TypeNameHandling.Objects` JSON オブジェクトをシリアライズするときに型名を追加する。クラスの定義がある場合、その型名が含まれています。

```
---- Object
{"$type":"SerializeSpike.Message, SerializeSpike","Id":1,"Name":"ushio","Context":{"$type":"SerializeSpike.Context, SerializeSpike","ActivityId":"abc"},"Books":["foo","bar"]}
```

他にも指定が可能です。

## 指定可能なハンドラ

| ハンドラ | 説明 |
| --------+ ------|
| None | 型名は含まない |
| Objects | オブジェクトに型名を含む |
| Arrays | JSON Array に型名を含む |
| All | すべてに型名を含む |
| Auto | 定義の型が実際にシリアライズされるときと違うものの型を含む。ただしRootのオブジェクトは含まない。 |

一瞬何が違うのかわかりませんが、動かしてみると明確です。

```csharp
            var messageJson = JsonConvert.SerializeObject(message);
            Console.WriteLine("---- normal");
            Console.WriteLine(messageJson);
            var messageJsonWithObjects = JsonConvert.SerializeObject(message, new JsonSerializerSettings()
            {
                TypeNameHandling = TypeNameHandling.Objects
            });
            Console.WriteLine("---- Object");
            Console.WriteLine(messageJsonWithObjects);
            var messageJsonWithArrays = JsonConvert.SerializeObject(message, new JsonSerializerSettings()
            {
                TypeNameHandling = TypeNameHandling.Arrays
            });
            Console.WriteLine("---- Array");
            Console.WriteLine(messageJsonWithArrays);
            var messageJsonWithAll = JsonConvert.SerializeObject(message, new JsonSerializerSettings()
            {
                TypeNameHandling = TypeNameHandling.All
            });
            Console.WriteLine("---- All");
            Console.WriteLine(messageJsonWithAll);
            var messageJsonWithAuto = JsonConvert.SerializeObject(message, new JsonSerializerSettings()
            {
                TypeNameHandling = TypeNameHandling.Auto
            });
            Console.WriteLine("---- Auto");
            Console.WriteLine(messageJsonWithAuto);
            Console.ReadLine();
```


## 実行結果

```
---- normal
{"Id":1,"Name":"ushio","Context":{"ActivityId":"abc"},"Books":["foo","bar"]}
---- Object
{"$type":"SerializeSpike.Message, SerializeSpike","Id":1,"Name":"ushio","Context":{"$type":"SerializeSpike.Context, SerializeSpike","ActivityId":"abc"},"Books":["foo","bar"]}
---- Array
{"Id":1,"Name":"ushio","Context":{"ActivityId":"abc"},"Books":{"$type":"System.Collections.Generic.List`1[[System.String, System.Private.CoreLib]], System.Private.CoreLib","$values":["foo","bar"]}}
---- All
{"$type":"SerializeSpike.Message, SerializeSpike","Id":1,"Name":"ushio","Context":{"$type":"SerializeSpike.Context, SerializeSpike","ActivityId":"abc"},"Books":{"$type":"System.Collections.Generic.List`1[[System.String, System.Private.CoreLib]], System.Private.CoreLib","$values":["foo","bar"]}}
---- Auto
{"Id":1,"Name":"ushio","Context":{"ActivityId":"abc"},"Books":{"$type":"System.Collections.Generic.List`1[[System.String, System.Private.CoreLib]], System.Private.CoreLib","$values":["foo","bar"]}}
```

# 何がうれしいのか？

最初はこの機能が何がうれしいのかさっぱりわかりませんでしたが、実際のコードをいじっているときにわかりました。この使い出は、インターフェイスがプロパティとして定義されているようなクラスで有効です。一旦 JSON になってしまえば、型がわかりません。インターフェイスに対してデシリアライズしようとすると、具象型がわからないので、デシリアライズできません。ところが型情報があれば、デシリアライズ可能です。このことを念頭に置くと、`Auto`が存在する理由もわかるでしょう。

# まとめ

今回は JSON に関するコードリーディングの内容をまとめてみました。