System.Text.JsonのJsonDocumentを使ってみる

# 概要
最近の.NET界隈ではJSONをいじくりまわしたいときはSystem.Text.Jsonを使うのがおすすめのようだ。
たまたま使う機会があったので調べてみたところ、主に以下のことができるみたい。

1. JSON文字列のシリアライズ/デシリアライズ
1. JsonDocumentによる読み取り専用JSON DOMを使った高速なデータアクセス
1. JsonNodeによる読み書き可能なJSON DOMを使ったデータ編集

今回は「2. JsonDocumentによる読み取り専用JSON DOMを使った高速なデータアクセス」についてまとめる。

# JsonDocumentとJsonNode
そもそもJsonDocumentとは何なのか。Microsoft Docsにわかりやすく書いてある。
> `JsonDocument`を使用すると、`Utf8JsonReader`を使用して読み取り専用DOMを構築することができます。
> `JsonDocument` DOMでは、そのデータにより高速にアクセスできます。

対するJsonNodeは以下のように説明されている。
> `JsonNode`およびその派生クラスを使用すると、変更可能なDOMを作成することができます。
> `JsonNode` DOMは作成後に変更できます。`JsonDocument` DOMは変更できません。

https://docs.microsoft.com/ja-jp/dotnet/standard/serialization/system-text-json-use-dom-utf8jsonreader-utf8jsonwriter?pivots=dotnet-6-0

JsonファイルやJSON文字列を読むだけなら`JsonDocument`、編集もしたいなら`JsonNode`という感じ。
確かに触ってみると`JsonDocument`の機能はかなりサッパリしている。ただ、よくよく調べてみると`JsonDocument`でも簡単な編集はできるようだ。

姉妹記事もご参照ください。

https://qiita.com/TheParkSider/items/9632c7c5cb4c5b3d9717

# テスト用データ
会社情報(`companies`)や社員情報(`employees`)からなる謎のデータ。
``` JSON
{
    "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やるのってどうやるんだろう…

``` C#
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)を追加してみる。

``` C#
(省略)
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);
    }
}
```

出力
``` JSON
{
  "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`に書き込む値を制御すればよいわけだが、ちょっと複雑すぎる気もする。