【C#】System.Text.Json でJSONを扱う方法

#### System.Text.Json でJSONを扱ってみよう

　今回は、JSONを取り扱う処理です。

##### C#のJSONの取り扱いの歴史

- `System.Runtime.Serialization.Json.DataContractJsonSerializer`
- 長く使われてきたサードパーティ製ライブラリの`Json.NET` (NuGetで取り込むときのライブラリ名は`Newtonsoft.Json`)
- C#4.0で導入されたライブラリの`Dynamic.Json`

　ググると上記が入り混じってヒットするかと思います。特に`Json.NET`が広く使われたのでよくヒットしますね。

　2015/11リリースの `.NET Framework 4.6.1` から`System.Text.Json`が導入されパッケージ管理のNuGetでインストールすると使えるようになり、実行環境自体が刷新された `.NET Core` では2017/8リリースの `.NET Core 2.0` から標準搭載。2021年からははこの **`System.Text.Json` がデフォルトで推奨**となっています。  
　どれも名前にJSONが入っていてややこしくコードでの書き方も違うので、検索する時はご注意ください。



##### 追加方法

　対象のフレームワークが `.NET Framework 4.7.*` や4.6のプロジェクトはツリーから[参照]-[参照の追加]で開く参照マネージャー、[アセンブリ]-[拡張]で `System.Text.Encodings.Web`, `System.Text.Json`にチェックを入れます。  
　`.NET Core 3.*` のプロジェクトだとツリーに参照がなく最初から使えますね。  
　いずれにせよコード本体の方でも`using`して名前空間を指定します。

　以下、サンプルコードは`.NET Core 3.1`でやっていますが`.NET Framework 4.7.*` でも同じです。


#### 構造が簡単なDictionary型→JSONへの変換

　C#では連想配列を表す辞書型の`Dictionary`型を使います。ここで型を明示的に指定しなければなりません。  
　変換自体は`System.Text.JsonSerializer`クラスの`Serialize`メソッドを固定で呼ぶだけです。このエントリの例では変換専用の`JsonUtilSample`クラスに集約しています。

https://docs.microsoft.com/ja-jp/dotnet/api/system.text.json.jsonserializer

　`Serialize`メソッドの第2引数でオプションが採れます。オプションはコード例のように指定すると、日本語も正しく表示、かつインデントされて綺麗に表示できます。他にもいろいろオプションがあります。

https://docs.microsoft.com/ja-jp/dotnet/api/system.text.json.jsonserializeroptions

```cs
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Unicode;

namespace JsonSample
{
    public class JsonUtilSample
    {
        /// <summary>
        /// 入力をJSON文字列に変換します。
        /// </summary>
        /// <param name="dict">Dictionary<string, string>型の入力</param>
        /// <returns>JSON文字列</returns>
        public static string ToJson(Dictionary<string, string> dict)
        {
            var json = JsonSerializer.Serialize(dict, JsonUtilSample.GetOption());
            return json;
        }

        /// <summary>
        /// 入力をJSON文字列に変換します。
        /// </summary>
        /// <param name="dict">Dictionary<string, int>型の入力</param>
        /// <returns>JSON文字列</returns>
        public static string ToJson(Dictionary<string, int> dict)
        {
            var json = JsonSerializer.Serialize(dict, JsonUtilSample.GetOption());
            return json;
        }
        
        /// <summary>
        /// オプションを設定します。内部メソッドです。
        /// </summary>
        /// <returns>JsonSerializerOptions型のオプション</returns>
        private static JsonSerializerOptions GetOption()
        {
            // ユニコードのレンジ指定で日本語も正しく表示、インデントされるように指定
            var options = new JsonSerializerOptions
            {
                Encoder = JavaScriptEncoder.Create(UnicodeRanges.All),
                WriteIndented = true,
            };
            return options;
        }
    }
}
```

呼び出し側のコードが以下。
```cs
var dict1 = new Dictionary<string, string>();
dict1.Add("fruits-1", "いちご");
dict1.Add("fruits-2", "りんご");
Console.WriteLine(JsonUtilSample.ToJson(dict1));

var dict2 = new Dictionary<string, int>();
dict2.Add("国語", 80);
dict2.Add("英語", 90);
Console.WriteLine(JsonUtilSample.ToJson(dict2));
```

実行すると以下。
```
{
  "fruits-1": "いちご",
  "fruits-2": "りんご"
}
{
  "国語": 80,
  "英語": 90
}
```

　簡単ですね。しかしキーに対応する値の型が複数あったり入れ子の配列になったり、JSONが複雑だともう使えません。


#### 構造が簡単なJSON→Dictionary型への変換

　`Serialize`の逆は`Deserialize`ということで、同じように呼べば変換できます。変換専用の`JsonUtilSample`クラス側の例が以下。  

```cs
/// <summary>
/// 入力のJSON文字列をDictionary型に変換します。
/// </summary>
/// <param name="json">JSON文字列</param>
/// <returns>Dictionary<string, string>型の出力(入力が異常の場合は空のオブジェクト)</returns>
public static Dictionary<string, string> JsonToDict(string json)
{
    if (String.IsNullOrEmpty(json))
    {
        return new Dictionary<string, string>();
    }
    try
    {
        Dictionary<string, string> dict = JsonSerializer.Deserialize<Dictionary<string, string>>(json, JsonUtilSample.GetOption());
        return dict;
    }
    catch (JsonException e)
    {
        Console.WriteLine(e.Message);
        return new Dictionary<string, string>();
    }
}
```

呼び出し側のコードが以下。
```cs
string jsonStr = @"
{
  ""fruits-3"": ""バナナ"",
  ""fruits-4"": ""ぶどう""
}
";
var dict = JsonUtilSample.JsonToDict(jsonStr);
foreach (string key in dict.Keys)
{
    Console.WriteLine("キー: " + key + " 値: " + dict[key]);
}

```


実行すると以下で、Dictionary型オブジェクトの中身がちゃんと入っています。

```
キー: fruits-3 値: バナナ
キー: fruits-4 値: ぶどう
```

　なお入力のJSONを文字列で定義している呼び出し側コード、`""ぶどう"",`のように最後の列も`,`を入れると、`System.Text.Json.JsonException` がスローされました。これくらい許容してほしい気もしますが厳密ですね……

　こちらも戻り値の`Dictionary`型の宣言時に型を決める必要があるので、例えば `Dictionary<string, int>` に変換したいのであれば別メソッドが必要になります。  
　じゃあ複雑なJSONだったらどうすんねんという話ですが...`Dictionary`の方で工夫する手もありそうですが、基本的にはオブジェクト指向に基づいてちゃんとクラスを定義する形になります。  
　実際に使った時は、少しでも複雑になった時は無理に`Dictionary`型でやろうとせず素直にクラスを定義したほうがスムーズでした。


#### 定義したクラスオブジェクト→JSONへの変換

　ということで入力を`Dictionary`型でなくクラスにします。変換するユーティリティ側は変わりません。  
　引数の型を固有のクラスごとに別メソッドで定義することもできますが、`Serialize()`メソッドに渡すのは`Object`型で大丈夫でした。

```cs
/// <summary>
/// 入力をJSON文字列に変換します。
/// </summary>
/// <param name="poco">定義済みのクラスオブジェクト</param>
/// <returns>JSON文字列 (入力が異常な場合はnull)</returns>
public static string ToJson(Object poco)
{
    try
    {
        var json = JsonSerializer.Serialize(poco, JsonUtilSample.GetOption());
        return json;
    }
    catch (JsonException e)
    {
        Console.WriteLine(e.Message);
        return null;
    }
}
```

 ユーザー1名の情報を想定したクラス定義の例 `SampleUserPoco` が以下です。 
データを持つだけで処理を持たない簡単なクラスをJavaの文化で`POJO(Plain Old Java Object)`と呼び、この流れでC#では`POCO(Plain Old CLR Object)`、Rubyでは`PORO`と呼びます。この記事でもクラス名などにpocoを使ってみます。呼び方は「ポコ」、よりイングリッシュな感じでは「ポゥコゥ」だそうです。

https://ja.wikipedia.org/wiki/Plain_Old_CLR_Object

　クラスの定義は簡単で特定クラスの継承なども不要。`using System.Text.Json.Serialization;` して、各フィールドの上に属性(=Javaのアノテーション)で実際のJSONのキー名との対応を書きます。  
C#でのフィールド名の命名規則は小文字始まりのローワーキャメルケース、setter/getterを自動的に付けてくれるプロパティ形式にすると大文字始まりのアッパーキャメル。  
　JSONのキー名はふつう小文字で始めることが多いので、大抵はこのように`JsonPropertyName`属性の`属性パラメータ側が小文字始まり <==> C#のプロパティ側は大文字`、という対応になります。

```cs
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Json.Serialization;

namespace HttpClientSample
{
    /// <summary>
    /// ユーザー情報を表すJSON全体に対応したPOCOなクラスの例。
    /// フィールド(メンバ変数名)がJSONのキーと対応しています。このクラスはデータのみで処理を行いません。
    /// </summary>
    public class SampleUserPoco
    {
        // 文字列型
        [JsonPropertyName("token")]
        public string Token { get; set; }
        [JsonPropertyName("userName")]
        public string UserName { get; set; }
        // 真偽値型
        [JsonPropertyName("isExcellent")]
        public bool IsExcellent { get; set; }
        // 数値型
        [JsonPropertyName("someIntValue")]
        public int SomeIntValue { get; set; }
        [JsonPropertyName("someDoubleValue")]
        public double? SomeDoubleValue { get; set; }
        // リスト型
        [JsonPropertyName("kvs")]
        public IList<SampleKvPoco> Kvs { get; set; }
    }
}
```

　注意点としては、**nullが入る可能性のあるフィールド**は上の`double?`のように`?`キーワードを付けます。  
　つけておかないとクラスオブジェクト→JSON文字列への変換では問題ありませんが、JSON文字列→クラスオブジェクトへの変換で失敗します。

　中で入れ子にして配列も持てるので、上の例では`IList<SampleKvPoco>`型で持っています。キーと値だけ持った配列の中の１件１件を表すクラスが以下。

```cs
    /// <summary>
    /// キーと値を持つJSON全体に対応したPOCOなクラスの例。
    /// フィールド(メンバ変数名)がJSONのキーと対応しています。このクラスはデータのみで処理を行いません。
    /// </summary>
    public class SampleKvPoco
    {
        [JsonPropertyName("key")]
        public string Key { get; set; }
        [JsonPropertyName("value")]
        public string Value { get; set; }
    }
```

これらのPOCOなクラスをパパウパウパウ...じゃなくてポゥコゥッと作って変換してみましょう。オブジェクト初期化子って便利ですね……

```cs
// C# 3.0から使えるオブジェクト初期化子を使ってオブジェクトを生成してみる
var poco = new SampleUserPoco {Token = "token_qwertyuiop@[", UserName = "Alice", IsExcellent = true,
    SomeIntValue = 10, SomeDoubleValue = 12.345678901};
var kvList = new List<SampleKvPoco>();
kvList.Add(new SampleKvPoco { Key = "key-1", Value = "value-1" });
kvList.Add(new SampleKvPoco { Key = "key-2", Value = "value-2" });
poco.Kvs = kvList;
// ポコッと作ったオブジェクトをJSONに変換！
Console.WriteLine(JsonUtilSample.ToJson(poco));
```

実行結果の文字列が以下です。綺麗に変換されています。メメタァ...
```
{
  "token": "token_qwertyuiop@[",
  "userName": "Alice",
  "isExcellent": true,
  "someIntValue": 10,
  "someDoubleValue": 12.345678901,
  "kvs": [
    {
      "key": "key-1",
      "value": "value-1"
    },
    {
      "key": "key-2",
      "value": "value-2"
    }
  ]
}
```

#### JSON→定義したクラスオブジェクトへの変換

　`Deserialize`メソッド呼び出し時に明確に型を指定すれば同じように変換できます。変換専用の`JsonUtilSample`クラス側の例が以下。

```cs
/// <summary>
/// 入力のJSON文字列をクラスに変換します。
/// </summary>
/// <param name="json">JSON文字列</param>
/// <returns>SampleUserPoco型の出力</returns>
public static SampleUserPoco JsonToSampleUserPoco(string json)
{
    if (String.IsNullOrEmpty(json))
    {
        return null;
    }
    try
    {
        SampleUserPoco poco = JsonSerializer.Deserialize<SampleUserPoco>(json, JsonUtilSample.GetOption());
        return poco;
    }
    catch (JsonException e)
    {
        Console.WriteLine(e.Message);
        return null;
    }
}
```

呼び出し側が以下。文字列でJSONを指定するところで、`someDoubleValue`をnullに変えてみましょう。
```cs
string jsonStr = @"
{
  ""token"": ""token_qwertyuiop@["",
  ""userName"": ""Alice"",
  ""isExcellent"": true,
  ""someIntValue"": 10,
  ""someDoubleValue"": null,
  ""kvs"": [
    {
      ""key"": ""key-1"",
      ""value"": ""value-1""
    },
    {
      ""key"": ""key-2"",
      ""value"": ""value-2""
    }
  ]
}
";
var poco = JsonUtilSample.JsonToSampleUserPoco(jsonStr);
Console.WriteLine("---クラスオブジェクトに変換した結果"
    + " token:"+ poco.Token + " userName:" + poco.UserName
    + " isExcellent:" + poco.IsExcellent + " someIntValue:" + poco.SomeIntValue
    + " someDoubleValue:" + poco.SomeDoubleValue + " kvs count:" + poco.Kvs.Count
    );
```



実行した結果が以下。標準出力の改行を忘れましたが、ちゃんとクラスオブジェクトに変換されています。
```
---クラスオブジェクトに変換した結果 token:token_qwertyuiop@[ userName:Alice isExcellent:True someIntValue:10 someDoubleValue: kvs count:2
```

`SampleUserPoco`クラスの定義で`someDoubleValue`フィールドの型を `double?` でなく `double` にしてしまうと、上記のように入力にnullが入ってきた時に以下のように変換時に失敗してしまうので注意です。

```
The JSON value could not be converted to System.Double. Path: $.someDoubleValue | LineNumber: 6 | BytePositionInLine: 25.
```

　実際の開発では例えばAPIを叩いてJSON形式でユーザー1件の情報が諸々返ってきて、中を取り出してあれやこれや……と本格的に処理するのであれば、C#でやるならこのようにちゃんとクラスを定義して扱った方が取り回しがしやすくなります。


#### まとめ：`System.Text.Json` で割と簡単にJSONを扱えるよ

　最後のJSON→定義したクラスオブジェクトへの変換 ですが、このやり方で行くとクラスごとに変換メソッドが別に必要になります。  
　僕も試したのですが`System.Text.Json.JsonElement`構造体`(struct)`を型指定するとメソッド1つでも行けました。しかし中身がうまく取り出せなかったり、そもそもC#の構造体ってそんなに使うものだっけ？(自分は使ったことがない/笑)というのもあってそこまでにしています。うまいやり方があったら知りたいところですね。

　ということでちゃんとクラスを定義して使えば `System.Text.Json` でスムーズにJSONも扱えるよということで、この記事は終わりなのです。


コード全体は以下のリポジトリにあります。

https://github.com/iwasiman/CSharp-HttpClient-json-sample


##### 最後にコード全体

変換を行うクラスのサンプル
```cs
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Unicode;

namespace HttpClientSample
{
    /// <summary>
    /// JSON周りの処理を行うユーティリティのサンプルクラス。
    /// </summary>
    public class JsonUtilSample
    {
        /// <summary>
        /// 入力をJSON文字列に変換します。
        /// </summary>
        /// <param name="dict">Dictionary<string, string>型の入力</param>
        /// <returns>JSON文字列</returns>
        public static string ToJson(Dictionary<string, string> dict)
        {
            var json = JsonSerializer.Serialize(dict, JsonUtilSample.GetOption());
            return json;
        }

        /// <summary>
        /// 入力をJSON文字列に変換します。
        /// </summary>
        /// <param name="dict">Dictionary<string, int>型の入力</param>
        /// <returns>JSON文字列</returns>
        public static string ToJson(Dictionary<string, int> dict)
        {
            var json = JsonSerializer.Serialize(dict, JsonUtilSample.GetOption());
            return json;
        }

        /// <summary>
        /// オプションを設定します。内部メソッドです。
        /// </summary>
        /// <returns>JsonSerializerOptions型のオプション</returns>
        private static JsonSerializerOptions GetOption()
        {
            // ユニコードのレンジ指定で日本語も正しく表示、インデントされるように指定
            var options = new JsonSerializerOptions
            {
                Encoder = JavaScriptEncoder.Create(UnicodeRanges.All),
                WriteIndented = true,
            };
            return options;
        }

        /// <summary>
        /// 入力のJSON文字列をDictionary型に変換します。
        /// </summary>
        /// <param name="json">JSON文字列</param>
        /// <returns>Dictionary<string, string>型の出力(入力が異常の場合は空のオブジェクト)</returns>
        public static Dictionary<string, string> JsonToDict(string json)
        {
            if (String.IsNullOrEmpty(json))
            {
                return new Dictionary<string, string>();
            }
            try
            {
                Dictionary<string, string> dict = JsonSerializer.Deserialize<Dictionary<string, string>>(json, JsonUtilSample.GetOption());
                return dict;
            }
            catch (JsonException e)
            {
                Console.WriteLine(e.Message);
                return new Dictionary<string, string>();
            }
        }

        /// <summary>
        /// 入力をJSON文字列に変換します。
        /// </summary>
        /// <param name="poco">定義済みのクラスオブジェクト</param>
        /// <returns>JSON文字列 (入力が異常な場合はnull)</returns>
        public static string ToJson(Object poco)
        {
            try
            {
                var json = JsonSerializer.Serialize(poco, JsonUtilSample.GetOption());
                return json;
            }
            catch (JsonException e)
            {
                Console.WriteLine(e.Message);
                return null;
            }
        }

        /// <summary>
        /// 入力のJSON文字列をクラスに変換します。
        /// </summary>
        /// <param name="json">JSON文字列</param>
        /// <returns>SampleUserPoco型の出力</returns>
        public static SampleUserPoco JsonToSampleUserPoco(string json)
        {
            if (String.IsNullOrEmpty(json))
            {
                return null;
            }
            try
            {
                SampleUserPoco poco = JsonSerializer.Deserialize<SampleUserPoco>(json, JsonUtilSample.GetOption());
                return poco;
            }
            catch (JsonException e)
            {
                Console.WriteLine(e.Message);
                return null;
            }
        }
    }
}
```

呼び出し側のクライアントのサンプル
```cs
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Json;

namespace HttpClientSample
{
    // JSONの変換クラスを呼びだす側のコードのサンプルです。
    public class JsonClient
    {
        static void Main(string[] args)
        {
            JsonClient.processSimpleDictToJson();
            JsonClient.processSimpleJsonToDict();
            JsonClient.procesPocoToJson();
            JsonClient.processJsonToSampleUserPoco();
        }

        private static void processSimpleDictToJson()
        {
            var dict1 = new Dictionary<string, string>();
            dict1.Add("fruits-1", "いちご");
            dict1.Add("fruits-2", "りんご");
            Console.WriteLine(JsonUtilSample.ToJson(dict1));

            var dict2 = new Dictionary<string, int>();
            dict2.Add("国語", 80);
            dict2.Add("英語", 90);
            Console.WriteLine(JsonUtilSample.ToJson(dict2));
        }

        private static void processSimpleJsonToDict()
        {
            string jsonStr = @"
{
  ""fruits-3"": ""バナナ"",
  ""fruits-4"": ""ぶどう""
}
";
            var dict = JsonUtilSample.JsonToDict(jsonStr);
            foreach (string key in dict.Keys)
            {
                Console.WriteLine("キー: " + key + " 値: " + dict[key]);
            }
        }

        private static void procesPocoToJson()
        {
            // C# 3.0から使えるオブジェクト初期化子を使ってオブジェクトを生成してみる
            var poco = new SampleUserPoco {Token = "token_qwertyuiop@[", UserName = "Alice", IsExcellent = true,
                SomeIntValue = 10, SomeDoubleValue = 12.345678901};
            var kvList = new List<SampleKvPoco>();
            kvList.Add(new SampleKvPoco { Key = "key-1", Value = "value-1" });
            kvList.Add(new SampleKvPoco { Key = "key-2", Value = "value-2" });
            poco.Kvs = kvList;
            // ポコッと作ったオブジェクトをJSONに変換！
            Console.WriteLine(JsonUtilSample.ToJson(poco));
        }

        private static void processJsonToSampleUserPoco()
        {
            string jsonStr = @"
{
  ""token"": ""token_qwertyuiop@["",
  ""userName"": ""Alice"",
  ""isExcellent"": true,
  ""someIntValue"": 10,
  ""someDoubleValue"": null,
  ""kvs"": [
    {
      ""key"": ""key-1"",
      ""value"": ""value-1""
    },
    {
      ""key"": ""key-2"",
      ""value"": ""value-2""
    }
  ]
}
";
            var poco = JsonUtilSample.JsonToSampleUserPoco(jsonStr);
            Console.WriteLine("---クラスオブジェクトに変換した結果"
                + " token:"+ poco.Token + " userName:" + poco.UserName
                + " isExcellent:" + poco.IsExcellent + " someIntValue:" + poco.SomeIntValue
                + " someDoubleValue:" + poco.SomeDoubleValue + " kvs count:" + poco.Kvs.Count
                );
        }
    }
}
```




この記事の元になったメインのブログでは他にも様々な記事を載せているので、こちらもどうぞお立ち寄りくださいませ。



https://iwasiman.hatenablog.com/entry/20210614-CSharp-json