概要
最近の.NET界隈ではJSONをいじくりまわしたいときはSystem.Text.Jsonを使うのがおすすめのようだ。
たまたま使う機会があったので調べてみたところ、主に以下のことができるみたい。
- JSON文字列のシリアライズ/デシリアライズ
- JsonDocumentによる読み取り専用JSON DOMを使った高速なデータアクセス
- JsonNodeによる読み書き可能なJSON DOMを使ったデータ編集
今回は「2. JsonDocumentによる読み取り専用JSON DOMを使った高速なデータアクセス」についてまとめる。
JsonDocumentとJsonNode
そもそもJsonDocumentとは何なのか。Microsoft Docsにわかりやすく書いてある。
JsonDocument
を使用すると、Utf8JsonReader
を使用して読み取り専用DOMを構築することができます。
JsonDocument
DOMでは、そのデータにより高速にアクセスできます。
対するJsonNodeは以下のように説明されている。
JsonNode
およびその派生クラスを使用すると、変更可能なDOMを作成することができます。
JsonNode
DOMは作成後に変更できます。JsonDocument
DOMは変更できません。
JsonファイルやJSON文字列を読むだけならJsonDocument
、編集もしたいならJsonNode
という感じ。
確かに触ってみるとJsonDocument
の機能はかなりサッパリしている。ただ、よくよく調べてみるとJsonDocument
でも簡単な編集はできるようだ。
姉妹記事もご参照ください。
テスト用データ
会社情報(companies
)や社員情報(employees
)からなる謎のデータ。
{
"version": "1.0.0",
"description": "企業情報",
"companies": [
{
"name": "A株式会社",
"address": "東京都○○市",
"employees": [
{
"name": "鈴木",
"age": 21,
"president": false
},
{
"name": "田中",
"age": 30,
"president": false
},
{
"name": "高橋",
"age": 54,
"president": true
}
]
},
{
"name": "株式会社B",
"address": "大阪府××市",
"employees": [
{
"name": "上田",
"age": 43,
"president": true
},
{
"name": "小沢",
"age": 27,
"president": false
}
]
},
{
"name": "C Co., Ltd.",
"address": "愛知県××市",
"employees": [
{
"name": "平井",
"age": 36,
"president": true
}
]
}
]
}
Json文字列を読み込んで分析する
上記のテストデータを読み込んで以下の処理をしている。
- JSONの構造を走査してすべての要素の型名と値を表示する
-
DisplayJsonElementRecursively()
を再帰的に呼び出す - オブジェクト(
JsonValueKind.Object
)や配列(JsonValueKind.Array
)には子要素があることに注意する
-
-
president
(社長)がtrue
になっている社員を列挙する- JSONの構造を泥臭く掘っていくだけ
-
TryXXX()
系のメソッドを使ってLINQやるのってどうやるんだろう…
using System;
using System.IO;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Unicode;
namespace JsonDocumentTest
{
internal class Program
{
internal static string jsonStr =
@"
{
""version"": ""1.0.0"",
""description"": ""企業情報"",
""companies"": [
{
""name"": ""A株式会社"",
""address"": ""東京都○○市"",
""employees"": [
{
""name"": ""鈴木"",
""age"": 21,
""president"": false
},
{
""name"": ""田中"",
""age"": 30,
""president"": false
},
{
""name"": ""高橋"",
""age"": 54,
""president"": true
}
]
},
{
""name"": ""株式会社B"",
""address"": ""大阪府××市"",
""employees"": [
{
""name"": ""上田"",
""age"": 43,
""president"": true
},
{
""name"": ""小沢"",
""age"": 27,
""president"": false
}
]
},
{
""name"": ""C Co., Ltd."",
""address"": ""愛知県××市"",
""employees"": [
{
""name"": ""平井"",
""age"": 36,
""president"": true
}
]
}
]
}
";
static void DisplayJsonElementRecursively(JsonElement element, int level = 0)
{
// オブジェクトのとき
if(element.ValueKind == JsonValueKind.Object)
{
string indent = new string('\t', ++level);
foreach (var child in element.EnumerateObject())
{
// オブジェクトの中身(JsonProperty型)を取り出す
Console.WriteLine($"{indent}Name: {child.Name}");
Console.WriteLine($"{indent}Kind: {child.Value.ValueKind}");
// JsonPropertyからJsonElementを取り出す
DisplayJsonElementRecursively(child.Value, level);
}
}
// 配列のとき
if(element.ValueKind == JsonValueKind.Array)
{
string indent = new string('\t', ++level);
foreach (var child in element.EnumerateArray())
{
// オブジェクトの中身(JsonElement)を取り出す
Console.WriteLine($"{indent}Kind: {child.ValueKind}");
// JsonElementをそのまま渡す
DisplayJsonElementRecursively(child, level);
}
}
// それ以外の型
if(element.ValueKind == JsonValueKind.Null ||
element.ValueKind == JsonValueKind.True ||
element.ValueKind == JsonValueKind.False ||
element.ValueKind == JsonValueKind.Number ||
element.ValueKind == JsonValueKind.String)
{
string indent = new string('\t', level);
Console.WriteLine($"{indent}Value: {element.ToString()}");
}
}
static void Main(string[] args)
{
JsonDocument doc = JsonDocument.Parse(jsonStr);
Console.WriteLine($"Name: root");
Console.WriteLine($"Kind: {doc.RootElement.ValueKind}");
// すべての要素の型名と値を再帰的に表示する
DisplayJsonElementRecursively(doc.RootElement);
// 社長(president)の名前を表示する
if(doc.RootElement.TryGetProperty("companies", out JsonElement companies))
{
foreach (var company in companies.EnumerateArray())
{
if (company.TryGetProperty("employees", out JsonElement employees) &&
company.TryGetProperty("name", out JsonElement companyName))
{
foreach (var employee in employees.EnumerateArray())
{
if (employee.TryGetProperty("president", out JsonElement president))
{
if (president.ValueKind == JsonValueKind.True ||
president.ValueKind == JsonValueKind.False)
{
if (president.GetBoolean() && employee.TryGetProperty("name", out JsonElement name))
{
Console.WriteLine($"President of {companyName.ToString()} is {name.ToString()}");
}
}
}
}
}
}
}
}
}
}
出力
Name: root
Kind: Object
Name: version
Kind: String
Value: 1.0.0
Name: description
Kind: String
Value: 企業情報
Name: companies
Kind: Array
Kind: Object
Name: name
Kind: String
Value: A株式会社
Name: address
Kind: String
Value: 東京都○○市
Name: employees
Kind: Array
Kind: Object
Name: name
Kind: String
Value: 鈴木
Name: age
Kind: Number
Value: 21
Name: president
Kind: False
Value: False
Kind: Object
Name: name
Kind: String
Value: 田中
Name: age
Kind: Number
Value: 30
Name: president
Kind: False
Value: False
Kind: Object
Name: name
Kind: String
Value: 高橋
Name: age
Kind: Number
Value: 54
Name: president
Kind: True
Value: True
Kind: Object
Name: name
Kind: String
Value: 株式会社B
Name: address
Kind: String
Value: 大阪府××市
Name: employees
Kind: Array
Kind: Object
Name: name
Kind: String
Value: 上田
Name: age
Kind: Number
Value: 43
Name: president
Kind: True
Value: True
Kind: Object
Name: name
Kind: String
Value: 小沢
Name: age
Kind: Number
Value: 27
Name: president
Kind: False
Value: False
Kind: Object
Name: name
Kind: String
Value: C Co., Ltd.
Name: address
Kind: String
Value: 愛知県××市
Name: employees
Kind: Array
Kind: Object
Name: name
Kind: String
Value: 平井
Name: age
Kind: Number
Value: 36
Name: president
Kind: True
Value: True
President of A株式会社 is 高橋
President of 株式会社B is 上田
President of C Co., Ltd. is 平井
JsonDocumentにデータを追加する
社員データに社員ID風の一意の値(GUID)を追加してみる。
(省略)
static void Main(string[] args)
{
JsonWriterOptions options = new JsonWriterOptions
{
Indented = true,
// これを指定しないと日本語が正しく出力されない
Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
};
using (MemoryStream ms = new MemoryStream())
using (Utf8JsonWriter writer = new Utf8JsonWriter(ms, options))
{
JsonDocument doc = JsonDocument.Parse(jsonStr);
// オブジェクトの開始'{'を書き込む
writer.WriteStartObject();
foreach (var child1 in doc.RootElement.EnumerateObject())
{
if (child1.Name == "companies")
{
writer.WritePropertyName(child1.Name);
// 配列の開始'['を書き込む
writer.WriteStartArray();
foreach (var child2 in child1.Value.EnumerateArray())
{
writer.WriteStartObject();
foreach (var child3 in child2.EnumerateObject())
{
if (child3.Name == "employees")
{
writer.WritePropertyName(child3.Name);
writer.WriteStartArray();
foreach (var child4 in child3.Value.EnumerateArray())
{
writer.WriteStartObject();
writer.WritePropertyName("id");
// GUIDを発行
writer.WriteStringValue(Guid.NewGuid().ToString());
foreach (var child5 in child4.EnumerateObject())
{
child5.WriteTo(writer);
}
writer.WriteEndObject();
}
writer.WriteEndArray();
}
else
{
child3.WriteTo(writer);
}
}
writer.WriteEndObject();
}
writer.WriteEndArray();
}
else
{
child1.WriteTo(writer);
}
}
writer.WriteEndObject();
writer.Flush();
string jsonResult = Encoding.UTF8.GetString(ms.ToArray());
Console.WriteLine(jsonResult);
}
}
出力
{
"version": "1.0.0",
"description": "企業情報",
"companies": [
{
"name": "A株式会社",
"address": "東京都○○市",
"employees": [
{
"id": "04ec7efb-28e7-43c4-8f1a-43571d2910b6",
"name": "鈴木",
"age": 21,
"president": false
},
{
"id": "99d9ad44-ef92-4fac-b8f2-2efc1a79661c",
"name": "田中",
"age": 30,
"president": false
},
{
"id": "0c57ba31-5efd-4507-9731-9c2ee47bd98d",
"name": "高橋",
"age": 54,
"president": true
}
]
},
{
"name": "株式会社B",
"address": "大阪府××市",
"employees": [
{
"id": "85810492-172f-479f-b17e-130657796c80",
"name": "上田",
"age": 43,
"president": true
},
{
"id": "8be5c096-dede-4aae-ab27-d60c08bcfba9",
"name": "小沢",
"age": 27,
"president": false
}
]
},
{
"name": "C Co., Ltd.",
"address": "愛知県××市",
"employees": [
{
"id": "d8e5c415-428a-4ae2-b7ee-55846c95d883",
"name": "平井",
"age": 36,
"president": true
}
]
}
]
}
JSONのオブジェクトの開始/終了{}
や配列の開始/終了[]
を意識する必要がある。
削除したり変更したりする場合もUtf8JsonWriter
に書き込む値を制御すればよいわけだが、ちょっと複雑すぎる気もする。