.NET 6 で動的にJsonパースする標準クラス JsonNode

# C#で動的にJsonパースしたい！

C#でもクラスを準備することなくjsonを jqや python 、node.js のようにダックタイピングしたいことがあります。たとえば、

- C#クラスを準備するのが面倒。ネストが多くてPOCOクラスだらけになる。
- `int` なのに `"NULL"` や `"-" `が入ってくるWebAPIがいる。
- ちょっとデータ欲しいだけなのにコンバーター作ったり、属性つけたり面倒。
- APIが適当すぎてクラス定義できない・・・

.NET6 で動的にjsonを扱うクラス `JsonNode` が追加されています。その使い勝手をベンチマーク含めて他の方法と比較します。

## サンプルのjsonデータ

[HeartRails Express](http://express.heartrails.com/api.html#station) から [山手線の駅一覧](http://express.heartrails.com/api/json?method=getStations&line=JR%E5%B1%B1%E6%89%8B%E7%B7%9A) を使わせていただきました。このjsonから駅名一覧を抽出するサンプルを作っていきます。
jqコマンドなら `jq ".response.station[].name"` になります。

<details>
<summary>jsonデータ例(2021年11月現在)</summary><div>

```json
{
    "response": {
        "station": [
            {
                "name": "品川",
                "prefecture": "東京都",
                "line": "JR山手線",
                "x": 139.738999,
                "y": 35.62876,
                "postal": "1080075",
                "prev": null,
                "next": "大崎"
            },
            {
                "name": "大崎",
                "prefecture": "東京都",
                "line": "JR山手線",
                "x": 139.728439,
                "y": 35.619772,
                "postal": "1410032",
                "prev": "品川",
                "next": "五反田"
            },
            {
                "name": "五反田",
                "prefecture": "東京都",
                "line": "JR山手線",
                "x": 139.723822,
                "y": 35.625974,
                "postal": "1410022",
                "prev": "大崎",
                "next": "目黒"
            },
            {
                "name": "目黒",
                "prefecture": "東京都",
                "line": "JR山手線",
                "x": 139.715775,
                "y": 35.633923,
                "postal": "1410021",
                "prev": "五反田",
                "next": "恵比寿"
            },
            {
                "name": "恵比寿",
                "prefecture": "東京都",
                "line": "JR山手線",
                "x": 139.71007,
                "y": 35.646684,
                "postal": "1500013",
                "prev": "目黒",
                "next": "渋谷"
            },
            {
                "name": "渋谷",
                "prefecture": "東京都",
                "line": "JR山手線",
                "x": 139.701238,
                "y": 35.658871,
                "postal": "1500002",
                "prev": "恵比寿",
                "next": "原宿"
            },
            {
                "name": "原宿",
                "prefecture": "東京都",
                "line": "JR山手線",
                "x": 139.702592,
                "y": 35.670646,
                "postal": "1500001",
                "prev": "渋谷",
                "next": "代々木"
            },
            {
                "name": "代々木",
                "prefecture": "東京都",
                "line": "JR山手線",
                "x": 139.702042,
                "y": 35.683061,
                "postal": "1510051",
                "prev": "原宿",
                "next": "新宿"
            },
            {
                "name": "新宿",
                "prefecture": "東京都",
                "line": "JR山手線",
                "x": 139.700464,
                "y": 35.689729,
                "postal": "1600022",
                "prev": "代々木",
                "next": "新大久保"
            },
            {
                "name": "新大久保",
                "prefecture": "東京都",
                "line": "JR山手線",
                "x": 139.700261,
                "y": 35.700875,
                "postal": "1690073",
                "prev": "新宿",
                "next": "高田馬場"
            },
            {
                "name": "高田馬場",
                "prefecture": "東京都",
                "line": "JR山手線",
                "x": 139.703715,
                "y": 35.712677,
                "postal": "1690075",
                "prev": "新大久保",
                "next": "目白"
            },
            {
                "name": "目白",
                "prefecture": "東京都",
                "line": "JR山手線",
                "x": 139.706228,
                "y": 35.720476,
                "postal": "1710031",
                "prev": "高田馬場",
                "next": "池袋"
            },
            {
                "name": "池袋",
                "prefecture": "東京都",
                "line": "JR山手線",
                "x": 139.711085,
                "y": 35.730256,
                "postal": "1710021",
                "prev": "目白",
                "next": "大塚"
            },
            {
                "name": "大塚",
                "prefecture": "東京都",
                "line": "JR山手線",
                "x": 139.728584,
                "y": 35.731412,
                "postal": "1700005",
                "prev": "池袋",
                "next": "巣鴨"
            },
            {
                "name": "巣鴨",
                "prefecture": "東京都",
                "line": "JR山手線",
                "x": 139.739303,
                "y": 35.733445,
                "postal": "1700002",
                "prev": "大塚",
                "next": "駒込"
            },
            {
                "name": "駒込",
                "prefecture": "東京都",
                "line": "JR山手線",
                "x": 139.748053,
                "y": 35.736825,
                "postal": "1700003",
                "prev": "巣鴨",
                "next": "田端"
            },
            {
                "name": "田端",
                "prefecture": "東京都",
                "line": "JR山手線",
                "x": 139.761229,
                "y": 35.737781,
                "postal": "1140013",
                "prev": "駒込",
                "next": "西日暮里"
            },
            {
                "name": "西日暮里",
                "prefecture": "東京都",
                "line": "JR山手線",
                "x": 139.766857,
                "y": 35.731954,
                "postal": "1160013",
                "prev": "田端",
                "next": "日暮里"
            },
            {
                "name": "日暮里",
                "prefecture": "東京都",
                "line": "JR山手線",
                "x": 139.771287,
                "y": 35.727908,
                "postal": "1100001",
                "prev": "西日暮里",
                "next": "鶯谷"
            },
            {
                "name": "鶯谷",
                "prefecture": "東京都",
                "line": "JR山手線",
                "x": 139.778015,
                "y": 35.721484,
                "postal": "1100003",
                "prev": "日暮里",
                "next": "上野"
            },
            {
                "name": "上野",
                "prefecture": "東京都",
                "line": "JR山手線",
                "x": 139.777043,
                "y": 35.71379,
                "postal": "1100005",
                "prev": "鶯谷",
                "next": "御徒町"
            },
            {
                "name": "御徒町",
                "prefecture": "東京都",
                "line": "JR山手線",
                "x": 139.774727,
                "y": 35.707282,
                "postal": "1100005",
                "prev": "上野",
                "next": "秋葉原"
            },
            {
                "name": "秋葉原",
                "prefecture": "東京都",
                "line": "JR山手線",
                "x": 139.773288,
                "y": 35.698619,
                "postal": "1010028",
                "prev": "御徒町",
                "next": "神田"
            },
            {
                "name": "神田",
                "prefecture": "東京都",
                "line": "JR山手線",
                "x": 139.770641,
                "y": 35.691173,
                "postal": "1010044",
                "prev": "秋葉原",
                "next": "東京"
            },
            {
                "name": "東京",
                "prefecture": "東京都",
                "line": "JR山手線",
                "x": 139.766103,
                "y": 35.681391,
                "postal": "1000005",
                "prev": "神田",
                "next": "有楽町"
            },
            {
                "name": "有楽町",
                "prefecture": "東京都",
                "line": "JR山手線",
                "x": 139.763806,
                "y": 35.675441,
                "postal": "1000006",
                "prev": "東京",
                "next": "新橋"
            },
            {
                "name": "新橋",
                "prefecture": "東京都",
                "line": "JR山手線",
                "x": 139.758587,
                "y": 35.666195,
                "postal": "1050004",
                "prev": "有楽町",
                "next": "浜松町"
            },
            {
                "name": "浜松町",
                "prefecture": "東京都",
                "line": "JR山手線",
                "x": 139.757135,
                "y": 35.655391,
                "postal": "1050022",
                "prev": "新橋",
                "next": "田町"
            },
            {
                "name": "田町",
                "prefecture": "東京都",
                "line": "JR山手線",
                "x": 139.747575,
                "y": 35.645737,
                "postal": "1080023",
                "prev": "浜松町",
                "next": "高輪ゲートウェイ"
            },
            {
                "name": "高輪ゲートウェイ",
                "prefecture": "東京都",
                "line": "JR山手線",
                "x": 139.740651,
                "y": 35.635476,
                "postal": "1080075",
                "prev": "田町",
                "next": null
            }
        ]
    }
}
```

</div>
</details>

## 比較ライブラリ

以下のライブラリと比較します。Json.NETは外しました。

- JsonNode
- System.Text.Json(クラス利用)
- System.Text.Json(動的)
- DynaJson
- DynamicJson
- Utf8Json (dynamic)
- JmesPath.Net

# 利用例

## JsonNode

.NET6で標準ライブラリに入った [JsonNode](https://docs.microsoft.com/ja-jp/dotnet/api/system.text.json.nodes?view=net-6.0) です。jsonを連想配列のように扱うことが可能です。例えば今回のjsonで"品川"を得るには `node["response"]["station"][0]["name"]` というコードになります。

駅一覧抽出コード例は以下の通り。"station" を AsArray() で列挙可能にした後 Linqで抽出しています。

```csharp
using System.Text.Json.Nodes;

var node = JsonNode.Parse(jsonString);
var list = node["response"]["station"].AsArray()
    .Select(s => s["name"].ToString());
```

`ToString()` を使っていますが、本来は型で抽出するには `GetValue<T>()` を使うようです。  
存在しないキーにアクセスした場合はnullが返ってきます。

## System.Text.Json(クラス利用)
 
参考までにクラスを作る標準的な方法も入れておきます。
型を作ってデシリアライズ、駅名一覧をLinqで抽出。クラスを作るのが問題なければ順当な方法です。

```csharp
using System.Text.Json;

var stationInfo = JsonSerializer.Deserialize<StationInfo>(jsonString);
var list = stationInfo.response.station.Select(s => s.name);
```

クラス定義は [json2charp](https://json2csharp.com/)で作りました。キャメルケースでC#らしくないですが、属性つけたりJsonOption作るのが面倒なので許してください・・・。

```csharp
public class Station
{
    public string name { get; set; }
    public string prefecture { get; set; }
    public string line { get; set; }
    public double x { get; set; }
    public double y { get; set; }
    public string postal { get; set; }
    public string prev { get; set; }
    public string next { get; set; }
}

public class Response
{
    public List<Station> station { get; set; }
}

public class StationInfo
{
    public Response response { get; set; }
}
```

## System.Text.Json(動的)

System.Text.Jsonで動的にJsonを扱うには `JsonElement` を使ってプロパティにアクセスする形になります。1つずつJsonElementノードを下っていき、列挙までもっていったら Linq で抽出してます。クラス定義は不要になりましたが、可読性が低いです。ですがパフォーマンスは一番いいです（後述のベンチマーク参照）。

```csharp
using System.Text.Json;

var doc = JsonDocument.Parse(jsonString);
var list = doc.RootElement
    .GetProperty("response")
    .GetProperty("station")
    .EnumerateArray()
    .Select(s => s.GetProperty("name").GetString());
```

## DynaJson

[fujieda](https://roundwide.com/ja/nan-boxing-in-csharp/)さん作の高速な動的Jsonシリアライザ [DynaJson](https://github.com/fujieda/DynaJson) ライブラリです。使い勝手は下の DynamicJson 互換、、衝撃的なほど高速で低メモリー消費です。

```csharp
using DynaJson

var dyna = DynaJson.JsonObject.Parse(jsonString);
var list = new List<string>();
foreach (var station in dyna.response.station)
{
    list.Add(station.name);
}
```
存在しないプロパティ名へアクセスすると「RuntimeBinderException」が発生します。
そのため、`IsDefined()` を使ってキーがあるかチェックの必要があるケースもあるでしょう。

## DynamicJson

[neue.cc](https://neue.cc/2010/04/30_256.html)さん作のJsonライブラリ[DynamicJson](https://github.com/neuecc/DynamicJson)。サンプルコードはDynaJsonと同じなので省略します。DataContractJsonSerializer しかなかった時代に使わせてもらってました。

作者のneueccさんは下のUTF8Jsonをはじめ、[ZeroFormatter](https://neue.cc/2016/11/08_542.html) や [Messagepack for c#](https://github.com/neuecc/MessagePack-CSharp) などパフォーマンス改善に踏み込んでいてブログも楽しく読ませてもらってます。

## Utf8Json

最速と謳われる 同じくneueccさんのライブラリ [Utf8Json](https://neue.cc/2017/09/29_559.html) であえて動的に扱ってみました。
dynamicへデコードし this[]アクセサーを使ってます。

```csharp
var res = Utf8Json.JsonSerializer.Deserialize<dynamic>(jsonString);
var list = new List<string>();
foreach (var item in res["response"]["station"])
{
    list.Add(item["name"]);
}
```

`System.Text.Json` でもこのように dynamic へのデコードが出来るとよいのですが。


## JmesPath.Net

AWSやAzureを使っている方ならなじみ深いJsonクエリー言語 [JMESPath](https://jmespath.org/)も参考として使ってみました。駅名一覧はJMESPath記法で `"response.station[].name"` というクエリーになります。.NET用のライブラリは[こちら](https://github.com/jdevillard/JmesPath.Net)。内部的にNewtonsoft.Json.NETを利用しているそうです。
コードはこれまでで最短です。

```csharp
using DevLab.JmesPath;

var jmes = new JmesPath();
var result = jmes.Transform(jsonString, "response.station[].name");
// "[ "品川","大崎","五反田"... という「文字列」が返ってくる 
```

JmesPath記法を知っていれば簡潔に記載できますが、戻り値は駅一覧のJson文字列です（C#の配列ではなく「文字列」）。そのためベンチマークは参考までとします。

# ベンチマーク結果

```
|                    Method |      Mean |    Error |   StdDev |   Gen 0 |  Gen 1 | Allocated |
|-------------------------- |----------:|---------:|---------:|--------:|-------:|----------:|
|                  JsonNode |  71.85 us | 0.371 us | 0.347 us | 11.5967 |      - |     47 KB |
|          System.Text.Json |  45.29 us | 0.194 us | 0.172 us |  2.3193 | 0.0610 |     10 KB |
| (dynamic)System.Text.Json |  30.93 us | 0.194 us | 0.162 us |  4.3335 |      - |     18 KB |
|                  Dynajson |  30.56 us | 0.570 us | 0.904 us |  9.5825 | 0.3052 |     39 KB |
|               DynamicJson | 175.79 us | 1.029 us | 0.912 us | 16.8457 | 0.9766 |     70 KB |
|         (dynamic)Uft8Json |  60.46 us | 0.298 us | 0.249 us | 12.2681 | 0.5493 |     50 KB |
|                  JmesPath | 322.27 us | 4.488 us | 3.504 us | 65.4297 | 2.4414 |    268 KB |

```
コード量が少なく直感的なのは **JMESPath** ですが、結果が利用しずらく速度も遅めです。
最速は **DynaJson** 又は **System.Text.Jsonで動的** ですが、コーディングの容易さも考えると **DynaJson** もしくは **JsonNode** でしょうか。 DynaJsonは動的言語っぽく、JsonNodeはC#の言語仕様によくマッチしていると思います。 
**Utf8Json** のほうが速いのですが これを使うときはちゃんとしたものを作るときな気がします・・。
存在しないプロパティへアクセスした場合のエラー処理も選択時のポイントになると思います。DynaJsonは「RuntimeBinderException」が発生、JsonNodeは `null` が戻ってきます。