以前に書いた一連の記事をまとめた記事です。
C# で定義が未知の Json を扱う (.NET Framework / JObject) - rksoftware
C# で定義が未知の Json を扱う (.NET Framework / ExpandoObject) - rksoftware
C# で定義が未知の Json を扱う (.NET Core / System.Text.Json) - rksoftware
C# で定義が未知の Json を扱う (.NET Core / System.Text.Json / System.Dynamic.ExpandoObject) - rksoftware
■ 【過去】 .NET Framework 時代
Json を扱う際には .NET Framework 時代には Json.NET というライブラリがよく使われていました。このライブラリの Newtonsoft.Json.JsonConvert.DeserializeObject
メソッドで未知の Json を扱うことができました。
メソッドの定義は次のようになっています。
public static object? DeserializeObject(string value);
使う際には次のように dynamic
型変数に代入して使っていた方も多いのではないでしょうか?
dynamic document = Newtonsoft.Json.JsonConvert.DeserializeObject( @"{ ""prop1"": ""value1"", ""prop2"": ""value2""}"); Console.WriteLine(document.prop1); // value1 と出力される Console.WriteLine(document.prop2); // value2 と出力される
実際にはこの際、DeserializeObject
からの戻りは何だろうと次のコードで検証。
dynamic document = Newtonsoft.Json.JsonConvert.DeserializeObject( @"{ ""prop1"": ""value1"", ""prop2"": ""value2""}"); Console.WriteLine(((object)document).GetType().FullName); // Newtonsoft.Json.Linq.JObject
Newtonsoft.Json.Linq.JObject
になっていました。
プロパティを表示する
Newtonsoft.Json.Linq.JObject
は IDictionary<string, JToken?>
を実装しているので、Key と Value を foreach
で回せます。
Newtonsoft.Json.Linq.JObject document = (Newtonsoft.Json.Linq.JObject)Newtonsoft.Json.JsonConvert.DeserializeObject( @"{ ""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 // と出力される
プロパティを追加する
IDictionary
の Add
メソッドでプロパティを追加できます。
Newtonsoft.Json.Linq.JObject document = (Newtonsoft.Json.Linq.JObject)Newtonsoft.Json.JsonConvert.DeserializeObject( @"{ ""prop1"": ""value1"", ""prop2"": ""value2""}"); ((IDictionary<string, Newtonsoft.Json.Linq.JToken?>)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";
プロパティを削除する
IDictionary
の Remove
メソッドでプロパティを削除できます。
Newtonsoft.Json.Linq.JObject document = (Newtonsoft.Json.Linq.JObject)Newtonsoft.Json.JsonConvert.DeserializeObject( @"{ ""prop1"": ""value1"", ""prop2"": ""value2""}"); ((IDictionary<string, Newtonsoft.Json.Linq.JToken?>)document).Remove("prop1"); foreach (var m in document) Console.WriteLine($"key:{m.Key}, Value:{m.Value}"); // key:prop2, Value:value2 // と出力される
IDictionary<string, Newtonsoft.Json.Linq.JToken?>
を実装しているので特にキャストしなくても OK です。
document.Remove("prop1");
■ System.Dynamic.ExpandoObject を使ってみる
C# には実は dynamic の中身として使う System.Dynamic.ExpandoObject
クラスがいます。ここからははこの型を使ってみましょう。
型の指定、型の確認
dynamic document = Newtonsoft.Json.JsonConvert.DeserializeObject<System.Dynamic.ExpandoObject>( @"{ ""prop1"": ""value1"", ""prop2"": ""value2""}"); Console.WriteLine(((object)document).GetType().FullName); // System.Dynamic.ExpandoObject
System.Dynamic.ExpandoObject
になりました。
プロパティを表示する
普通に dynamic っぽく。
dynamic document = Newtonsoft.Json.JsonConvert.DeserializeObject<System.Dynamic.ExpandoObject>( @"{ ""prop1"": ""value1"", ""prop2"": ""value2""}"); Console.WriteLine(document.prop1); // value1 と出力される Console.WriteLine(document.prop2); // value2 と出力される
System.Dynamic.ExpandoObject
は IDictionary<string, object>
を実装しているので、Key と Value を foreach
で回せます。
System.Dynamic.ExpandoObject document = Newtonsoft.Json.JsonConvert.DeserializeObject<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 // と出力される
プロパティを追加する
IDictionary
の Add
メソッドでプロパティを追加できます。
System.Dynamic.ExpandoObject document = Newtonsoft.Json.JsonConvert.DeserializeObject<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";
プロパティを削除する
IDictionary
の Remove
メソッドでプロパティを削除できます。
System.Dynamic.ExpandoObject document = Newtonsoft.Json.JsonConvert.DeserializeObject<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 _);
■ 現行世代の Json API / System.Text.Json
さて、これまでは .NET Framework 時代の話をしてきました。今の時代 (.NET Core 3 以降) は System.Text.Json
を使用します。
補足 Json.NET は .NET Core でも使えます。パフォーマンスは System.Text.Json に劣るとのことですが使い勝手としてまだまだ Json.NET にメリットがある場面はありそうです
ここからは System.Text.Json
の
System.Text.Json.JsonDocument.Parse
メソッドによるSystem.Text.Json.JsonDocument
で扱うTValue System.Text.Json.JsonSerializer.Deserialize<TValue>
メソッドでSystem.Dynamic.ExpandoObject
を扱う
ことで未知の 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.ExpandoObject
は IDictionary<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 // と出力される
プロパティを追加する
IDictionary
の Add
メソッドでプロパティを追加できます。
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";
プロパティを削除する
IDictionary
の Remove
メソッドでプロパティを削除できます。
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 と出力される);
こんな感じです。
■ やっぱり System.Text.Json でも System.Dynamic.ExpandoObject を使いたい
System.Text.Json.JsonSerializer.Deserialize<System.Dynamic.ExpandoObject>
は多段に構造化された Json の場合、あまり嬉しくなかったので、雑に JsonDucument で デシリアライズした後に ExpandoObject に変えるコードを書いてみました。
### コンバートするコード
static System.Dynamic.ExpandoObject Parse(string json) { using var document = System.Text.Json.JsonDocument.Parse(json); return toExpandoObject(document.RootElement); static object propertyValue(System.Text.Json.JsonElement elm) => elm.ValueKind switch { System.Text.Json.JsonValueKind.Null => null, System.Text.Json.JsonValueKind.Number => elm.GetDecimal(), System.Text.Json.JsonValueKind.String => elm.GetString(), System.Text.Json.JsonValueKind.False => false, System.Text.Json.JsonValueKind.True => true, System.Text.Json.JsonValueKind.Array => elm.EnumerateArray().Select(m => propertyValue(m)).ToArray(), _ => toExpandoObject(elm), }; static System.Dynamic.ExpandoObject toExpandoObject(System.Text.Json.JsonElement elm) => elm.EnumerateObject() .Aggregate( new System.Dynamic.ExpandoObject(), (exo, prop) => { ((IDictionary<string, object>)exo).Add(prop.Name, propertyValue(prop.Value)); return exo; }); }
理屈は、まあ普通の C# コードなので読んでもらえば分かると思います。
使い方
メソッドなので普通に使えます。
dynamic expandoObject = Parse( @"{ ""prop1"": ""value1"", ""prop2"": 2, ""prop3"": { ""p31"": 31 , ""p32"": [1, ""2"", { ""p321"": ""value321"", ""p322"": ""value322"" } ] }}");
試しに、プロパティを追加したり削除したりなどして、シリアライズして結果を見てみましょう。
dynamic expandoObject = Parse( @"{ ""prop1"": ""value1"", ""prop2"": 2, ""prop3"": { ""p31"": 31 , ""p32"": [1, ""2"", { ""p321"": ""value321"", ""p322"": ""value322"" } ] }}"); // プロパティを追加してみる expandoObject.prop3.p33 = 33; // プロパティを削除してみる ((IDictionary<string, object>)expandoObject.prop3).Remove("p31"); // シリアライズしてみる Console.WriteLine( System.Text.Json.JsonSerializer.Serialize(expandoObject) ); // {"prop1":"value1","prop2":2,"prop3":{"p32":[1,"2",{"p321":"value321","p322":"value322"}],"p33":33}} と出力される
出力結果
{"prop1":"value1","prop2":2,"prop3":{"p32":[1,"2",{"p321":"value321","p322":"value322"}],"p33":33}}
それっぽく動いているようです。
このコンバートするコードは GitHub にもアップしています。 何か面白い活用のアイデアがあったら教えてください。
でもやっぱり
未知の構造の Json を扱う場合は、Json.NET を使ってしまうのが手っ取り早いかもしれません。