Rのつく財団入り口

ITエンジニア関連の様々な話題を書いているはずのブログです。

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

System.Text.JsonJSONを扱ってみよう

 突然現れる技術系っぽい記事シリーズ...ということで今回は、JSONを取り扱う処理です。

System.Text.JsonJSONを扱ってみよう

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クラスに集約しています。

docs.microsoft.com

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

docs.microsoft.com

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;
        }
    }
}

呼び出し側のコードが以下。

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クラス側の例が以下。

/// <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>();
    }
}

呼び出し側のコードが以下。

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型で大丈夫でした。

/// <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を使ってみます。呼び方は「ポコ」、よりイングリッシュな感じでは「ポゥコゥ」だそうです。

ja.wikipedia.org

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

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>型で持っています。キーと値だけ持った配列の中の1件1件を表すクラスが以下。

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

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

// 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クラス側の例が以下。

/// <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に変えてみましょう。

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も扱えるよということで、この記事は終わりなのです。

System.Text.JsonJSONを扱ってみよう

github.com

最後にコード全体

変換を行うクラスのサンプル

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;
            }
        }
    }
}

呼び出し側のクライアントのサンプル

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
                );
        }
    }
}
C#の関連書籍

こちらの記事もどうぞ

iwasiman.hatenablog.com

iwasiman.hatenablog.com

iwasiman.hatenablog.com

iwasiman.hatenablog.com