rksoftware

Visual Studio とか C# とかが好きです

C# で定義が未知の Json を扱う (.NET Core / System.Text.Json)

未知の Json を扱う一連の記事をまとめた記事を書きました。
こちらの記事で一気読みできます。

■ 現行世代の Json API / System.Text.Json

Json を扱う際には .NET Framework 時代には Json.NET というライブラリがよく使われていました。今の時代 (.NET Core 3 以降) は System.Text.Json を使用します。 今回は System.Text.Json

  • System.Text.Json.JsonDocument.Parse メソッドによる System.Text.Json.JsonDocument で扱う
  • TValue System.Text.Json.JsonSerializer.Deserialize<TValue> メソッドで System.Dynamic.ExpandoObject を扱う

ことで未知の Json を扱うことができました。

.NET Framework 時代で未知の Json を扱う記事を以前に書きました。 今回は同じようなことを System.Text.Json で試していきます。

■ System.Text.Json.JsonDocument

メソッドの定義は次のようになっています。

public static JsonDocument Parse(string json, JsonDocumentOptions options = default)

とりあえず以前の記事のように、dynamic 型変数に代入して使ってみましょう。

dynamic の中身

using dynamic document = System.Text.Json.JsonDocument.Parse(
    @"{ ""prop1"": ""value1"", ""prop2"": ""value2""}");

Console.WriteLine(((object)document).GetType().FullName);  // System.Text.Json.JsonDocument

中身の型は当たり前ですが、System.Text.Json.JsonDocument です。

プロパティ

プロパティを取り出そうとしてみましょう

using dynamic document = System.Text.Json.JsonDocument.Parse(
    @"{ ""prop1"": ""value1"", ""prop2"": ""value2""}");

// 実行時エラー
// Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: ''System.Text.Json.JsonDocument' does not contain a definition for 'prop1''
Console.WriteLine(document.prop1);
Console.WriteLine(document.prop2);

残念ながら、実行時に例外が発生します。

Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: ''System.Text.Json.JsonDocument' does not contain a definition for 'prop1''

プロパティは次のようにして取り出せます。

System.Text.Json.JsonDocument document = System.Text.Json.JsonDocument.Parse(
    @"{ ""prop1"": ""value1"", ""prop2"": ""value2""}");

Console.WriteLine(document.RootElement.GetProperty("prop1").GetRawText());  // "value1" と出力される
Console.WriteLine(document.RootElement.GetProperty("prop2").GetRawText());  // "value2" と出力される
Console.WriteLine(document.RootElement.GetProperty("prop1").GetString());   // value1 と出力される
Console.WriteLine(document.RootElement.GetProperty("prop2").GetString());   // value2 と出力される

■ System.Text.Json.JsonSerializer.Deserialize

最初に結論を出しておくと、Json に階層化された構造があると、嬉しい結果になりません。その詳細は後述します。

メソッドの定義は次のようになっています。

public static TValue Deserialize<TValue>(string json, JsonSerializerOptions options = null);

とりあえず以前の記事のように、dynamic 型変数に代入して使ってみましょう。

dynamic の中身

もう敢えて確認する必要もないですが、System.Dynamic.ExpandoObject です。

dynamic document = System.Text.Json.JsonSerializer.Deserialize<System.Dynamic.ExpandoObject>(
    @"{ ""prop1"": ""value1"", ""prop2"": ""value2""}");

Console.WriteLine(document.GetType().FullName);  // System.Dynamic.ExpandoObject

プロパティを表示する

普通に dynamic っぽく。

dynamic document = System.Text.Json.JsonSerializer.Deserialize<System.Dynamic.ExpandoObject>(
    @"{ ""prop1"": ""value1"", ""prop2"": ""value2""}");

Console.WriteLine(document.prop1);  // value1 と出力される
Console.WriteLine(document.prop2);  // value2 と出力される

System.Dynamic.ExpandoObjectIDictionary<string, object> を実装しているので、Key と Value を foreach で回せます。

dynamic document = System.Text.Json.JsonSerializer.Deserialize<System.Dynamic.ExpandoObject>(
    @"{ ""prop1"": ""value1"", ""prop2"": ""value2""}");

foreach (var m in document)
    Console.WriteLine($"key:{m.Key}, Value:{m.Value}");

// key:prop1, Value:value1
// key:prop2, Value:value2
// と出力される

プロパティを追加する

IDictionaryAdd メソッドでプロパティを追加できます。

dynamic document = System.Text.Json.JsonSerializer.Deserialize<System.Dynamic.ExpandoObject>(
    @"{ ""prop1"": ""value1"", ""prop2"": ""value2""}");

((IDictionary<string, object>)document).Add("prop3", "value3");

foreach (var m in document)
    Console.WriteLine($"key:{m.Key}, Value:{m.Value}");

Console.WriteLine($"key:prop3, Value:{((dynamic)document).prop3}");
// key:prop1, Value:value1
// key:prop2, Value:value2
// key:prop3, Value:value3
// key:prop3, Value:value3
// と出力される

dynamic 型にして次のようにしてもできます。

((dynamic)document).prop3 = "value3";

プロパティを削除する

IDictionaryRemove メソッドでプロパティを削除できます。

System.Dynamic.ExpandoObject document = System.Text.Json.JsonSerializer.Deserialize<System.Dynamic.ExpandoObject>(
    @"{ ""prop1"": ""value1"", ""prop2"": ""value2""}");

((IDictionary<string, object>)document).Remove("prop1");

foreach (var m in document)
    Console.WriteLine($"key:{m.Key}, Value:{m.Value}");

// key:prop2, Value:value2
// と出力される

IDictionary<string, object> を実装しているので特にキャストしなくても OK です。

document.Remove("prop1", out var _);

■ System.Text.Json.JsonSerializer.Deserialize<System.Dynamic.ExpandoObject> の嬉しくないところ

Json に階層化された構造があると、嬉しい結果になりません。

dynamic document = System.Text.Json.JsonSerializer.Deserialize<System.Dynamic.ExpandoObject>(
    @"{ ""prop1"": ""a"", ""prop2"": 1, ""prop3"": { ""p31"":31 , ""p32"":32 }}");

// 実行時エラー
// Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: ''System.Text.Json.JsonElement' does not contain a definition for 'p31''
Console.WriteLine(document.prop3.p31);

実行時に例外が発生します。

Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: ''System.Text.Json.JsonElement' does not contain a definition for 'p31''

階層化構造の子の要素も System.Dynamic.ExpandoObject になってくれると嬉しいのですが、System.Text.Json.JsonElement になっています。もじ p31 プロパティの値が欲しければ次のような感じです。

dynamic document = System.Text.Json.JsonSerializer.Deserialize<System.Dynamic.ExpandoObject>(
    @"{ ""prop1"": ""a"", ""prop2"": 1, ""prop3"": { ""p31"":31 , ""p32"":32 }}");

Console.WriteLine(document.prop3.GetProperty("p31").GetRawText());  // 31 と出力される);
Console.WriteLine(document.prop3.GetProperty("p31").GetInt32());    // 31 と出力される);

もう少し丁寧に書くと

dynamic document = System.Text.Json.JsonSerializer.Deserialize<System.Dynamic.ExpandoObject>(
    @"{ ""prop1"": ""a"", ""prop2"": 1, ""prop3"": { ""p31"":31 , ""p32"":32 }}");

Console.WriteLine(document.prop3.GetProperty("p31").TryGetInt32(out int value) ? value : default); // 31 と出力される);

こんな感じです。