以前に書いた一連の記事をまとめた記事です。
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);
Console.WriteLine(document.prop2);
実際にはこの際、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
は 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}");
プロパティを追加する
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}");
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}");
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
になりました。
プロパティを表示する
普通に dynamic っぽく。
dynamic document = Newtonsoft.Json.JsonConvert.DeserializeObject<System.Dynamic.ExpandoObject>(
@"{ ""prop1"": ""value1"", ""prop2"": ""value2""}");
Console.WriteLine(document.prop1);
Console.WriteLine(document.prop2);
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}");
プロパティを追加する
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}");
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}");
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
です。
プロパティ
プロパティを取り出そうとしてみましょう
using dynamic document = System.Text.Json.JsonDocument.Parse(
@"{ ""prop1"": ""value1"", ""prop2"": ""value2""}");
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());
Console.WriteLine(document.RootElement.GetProperty("prop2").GetRawText());
Console.WriteLine(document.RootElement.GetProperty("prop1").GetString());
Console.WriteLine(document.RootElement.GetProperty("prop2").GetString());
■ 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);
プロパティを表示する
普通に dynamic っぽく。
dynamic document = System.Text.Json.JsonSerializer.Deserialize<System.Dynamic.ExpandoObject>(
@"{ ""prop1"": ""value1"", ""prop2"": ""value2""}");
Console.WriteLine(document.prop1);
Console.WriteLine(document.prop2);
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}");
プロパティを追加する
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}");
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}");
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 }}");
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());
Console.WriteLine(document.prop3.GetProperty("p31").GetInt32());
もう少し丁寧に書くと
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);
こんな感じです。
■ やっぱり 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}}
それっぽく動いているようです。
このコンバートするコードは GitHub にもアップしています。
何か面白い活用のアイデアがあったら教えてください。
でもやっぱり
未知の構造の Json を扱う場合は、Json.NET を使ってしまうのが手っ取り早いかもしれません。