System.Text.Jsonでクラスをシリアライズ/デシリアライズする

# 概要
最近の.NET界隈ではJSONをいじくりまわしたいときは**System.Text.Json**を使うのがおすすめのようだ。
たまたま使う機会があったので調べてみたところ、主に以下のことができるみたい。
1. JSON文字列のシリアライズ/デシリアライズ
1. JsonDocumentによる読み取り専用JSON DOMを使った高速なデータアクセス
1. JsonNodeによる読み書き可能なJSON DOMを使ったデータ編集

今回は「1. JSON文字列のシリアライズ/デシリアライズ」についてまとめる。

# シリアライズ
シリアライズはC#クラスやDictionaryなどのプログラムっぽいものからJSON文字列という単純なのに変換する作業である。
クラスを作ってそのインスタンスを`JsonSerializer`に渡すだけでJSON文字列を生成してくれる。

``` C#
using System.Text.Json;
using System.Text.Json.Serialization;

namespace JsonSerialize
{ 
    public class Data1
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public List<string> Family { get; set; }
    }

    public class Program
    {
        static void Main(string[] args)
        {
            var data1 = new Data1()
            {
                Name = "Tanaka",
                Age = 30,
                Family = new List<string>
                {
                    "Mother",
                    "Father",
                    "Sister"
                }
            };
            string jsonStr = JsonSerializer.Serialize(data1);
            Console.WriteLine(jsonStr);
        }
    }
}
```

# デシリアライズ
デシリアライズはJSON文字列のようなプログラムからちょっと扱いにくいモノを
C#クラスなどの扱いやすいものに変換する作業である。

``` C#
using System.Text.Json;
using System.Text.Json.Serialization;

namespace JsonSerialize
{
    public class Data1
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public List<string> Family { get; set; }
    }

    public class Program
    {
        static void Main(string[] args)
        {
            string jsonStr = @"{ ""Name"":""Tanaka"",""Age"":30,""Family"":[""Mother"",""Father"",""Sister""]}";
            var data1 = JsonSerializer.Deserialize<Data1>(jsonStr);
        }
    }
}
```

シリアライズしたいクラスのプロパティを読み取り専用にしたい(setさせたくない)場合、
そのままアクセサだけを変えるとすべてのプロパティにはデフォルト値が入ってしまってちゃんとデシリアライズされないので
**プロパティの名前と仮引数の名前が大文字小文字の違いを無視して一致した(Case-Insensitiveな)引数付きコンストラクタ**を用意する。
つまり、プロパティ名がNameなら仮引数名はNameやnameなどにしてやればよい。

``` C#
using System.Text.Json;
using System.Text.Json.Serialization;

namespace JsonSerialize
{
    public class Data1
    {
        // このコンストラクタが必要！
        public Data1(string name, int age, List<string> family)
        {
            Name = name;
            Age = age;
            Family = family;
        }
        // プロパティがセット不可
        public string Name { get; }
        public int Age { get; }
        public List<string> Family { get; }
    }

    public class Program
    {
        static void Main(string[] args)
        {
            string jsonStr = @"{ ""Name"":""Tanaka"",""Age"":30,""Family"":[""Mother"",""Father"",""Sister""]}";
            var data1 = JsonSerializer.Deserialize<Data1>(jsonStr);
        }
    }
}
```

ちなみに引数付きコンストラクタを作る場合、Visual Studioのショートカットキーが便利だ。
コンストラクタで初期化したいプロパティを選択して「Ctrl + .」を押下すると以下のようなメニューが表示される。
![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57609/562b63cd-b3ee-840b-ff6b-ea51e538a8dc.png)

なお、引数違いやデフォルトコンストラクタをクラス内に作ってしまうと
`JsonSerializer`がどれを使ってデシリアライズすればいいのかわからなくなってしまうので、デシリアライズに使ってほしいコンストラクタには`[JsonConstructor]`の属性をつけておこう。

``` C#
    public class Data1
    {
        // コンストラクタが増えた
        public Data1() { }
        
        [JsonConstructor]
        public Data1(string name, int age, List<string> family)
        {
            Name = name;
            Age = age;
            Family = family;
        }
        (省略)
```

# コンバータ
`JsonSerializer`はわりと優秀なのだが、どうしても複雑なクラスをシリアライズしたい場合などにうまくいかないことがある。
そういった場合はシリアライズ/デシリアライズを制御するコンバータを作ってやる。

``` C#
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Unicode;

namespace JsonSerialize
{
    public class Data1Converter : JsonConverter<Data1>
    {
        // デシリアライズに使うやつ
        public override Data1? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            if (JsonDocument.TryParseValue(ref reader, out JsonDocument? doc))
            {
                return new Data1
                {
                    Name = doc.RootElement.GetProperty("Name").GetString(),
                    Age = doc.RootElement.GetProperty("Age").GetInt32(),
                    Family = doc.RootElement.GetProperty("Family").EnumerateArray().Select(e => e.GetString()).ToList()
                };
            }
            return null;
        }

        // シリアライズに使うやつ
        public override void Write(Utf8JsonWriter writer, Data1 value, JsonSerializerOptions options)
        {
            JsonSerializer.Serialize(writer, value);
        }
    }

    public class Data1
    {
        public Data1() { }

        [JsonConstructor]
        public Data1(string name, int age, List<string> family)
        {
            Name = name;
            Age = age;
            Family = family;
        }
        public string Name { get; set; }
        public int Age { get; set; }
        public List<string> Family { get; set; }
    }

    public class Program
    {
        static void Main(string[] args)
        {
            string jsonStr = @"{ ""Name"":""Tanaka"",""Age"":30,""Family"":[""Mother"",""Father"",""Sister""]}";

            // オプションでコンバータを指定する
            JsonSerializerOptions options = new JsonSerializerOptions
            {
                // オプションは他にもいろいろ指定できる
                WriteIndented = true,
                Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
            };
            options.Converters.Add(new Data1Converter());

            // オプションを第2引数に指定する (シリアライズとデシリアライズでそれぞれ別のオプションでもよい)
            var data1 = JsonSerializer.Deserialize<Data1>(jsonStr, options);
            var data1Str = JsonSerializer.Serialize(data1, options);
        }
    }
}
```

`JsonConverter`クラスを継承したコンバータクラスを作ってやり、デシリアライズの処理を`Read()`に、シリアライズの処理を`Write()`に書く。
作成したコンバータークラスは`JsonSerializerOptions`の`Converters`のリストに追加してやると、そのクラスをシリアライズ/デシリアライズする際に自動的に参照してくれる。

上の例はコンバータは特に目立ったことは何もしておらず、コンバータを指定せずにデシリアライズする場合と同じ結果になるが、
小細工しようと思えばいろいろなことができる。

``` C#
namespace JsonSerialize
{
    public class Data1Converter : JsonConverter<Data1>
    {
        public override Data1? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            // 不正な入力を検出したり
            if(reader.TokenType == JsonTokenType.Null)
            {
                throw new JsonException();
            }
            if (JsonDocument.TryParseValue(ref reader, out JsonDocument? doc))
            {
                return new Data1
                {
                    // データを加工してみたり
                    Name = doc.RootElement.GetProperty("Name").GetString() + "さん",
                    Age = doc.RootElement.GetProperty("Age").GetInt32(),
                    Family = doc.RootElement.GetProperty("Family").EnumerateArray().Select(e => e.GetString()).ToList()
                };
            }
            return null;
        }

        public override void Write(Utf8JsonWriter writer, Data1 value, JsonSerializerOptions options)
        {
            // 手動でシリアライズ後の文字列をいじくりまわしてみたり
            writer.WriteStartObject();
            writer.WritePropertyName("Written");
            writer.WriteStringValue(DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"));
            writer.WritePropertyName("Root");
            JsonSerializer.Serialize(writer, value);
            writer.WriteEndObject();
        }
    }
    (省略)
```

ちなみにシリアライズによって下のようなJSONが出力される。
``` JSON
{
  "Written": "2022/05/02 00:26:26",
  "Root": {
    "Name": "Tanakaさん",
    "Age": 30,
    "Family": [
      "Mother",
      "Father",
      "Sister"
    ]
  }
}
```

あまりに実用性がない例ですみません…