.NET6のSystem.Text.Jsonでレスポンスのモデルクラスを作る手間をラクできるかな？

# AdventCalendarについて

✩本記事は [Qiita AdventCalendar2021 / 祝 .NET 6 GA！.NET 6 での開発 Tips や試してみたことなど、あなたの「いち推し」ポイントを教えてください【PR】日本マイクロソフト Advent Calendar 2021](https://qiita.com/advent-calendar/2021/microsoft) の 3日目の記事です

https://qiita.com/advent-calendar/2021/microsoft

## きのうは

昨日の本カレンダーの記事は @up-hash さんの「.NET6 が起動するまでのコードを追ってみよう」でした
https://qiita.com/up-hash/items/87e98261bb026298f207

これまで意識することのなかった.NETアプリの裏側を垣間見えて非常に面白い記事でしたね！

# はじめに

まずは.NET6のGA(General Availability)、おめでとうございます

さて、この記事を拝読されている方の中にはこんなご経験やお悩みをお持ちの方はいらっしゃいませんでしょうか

- APIの電文(json)のモデル作るのダルいなぁ
- というかそもそもAPIで返されるjsonの仕様が仕様書に無いからモデルクラス作ったとしてもデシリアライズするのミスること多いんだけど

こんなお悩み、無い方がもちろん良いのですが、もしあったとしても、もしかしたら.NET6で改善できるかもしれません

本記事では、正式リリースに至りました.NET6にて改善がなされた機能のひとつ、 `System.Text.Json` の主にデシリアライズについて記述します

## 実行環境

- VisualStudio2022
- macOS環境1
    - macOS12.0.1 Monterey (9thGen Corei5)
    - .NET6 SDK 6.0.100 (x86_64)
- macOS環境2
    - macOS12.0.1 Monterey (Apple M1Pro)
    - .NET6 SDK 6.0.100 (ARM64)

## サンプルコード

こちらに本記事を書くに当たって作成したコードのリポジトリがございます
よろしければご確認ください

https://github.com/cocoalix/QiitaAdvCal2021Dec03DotNet6

## 参考

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

# Section0: カルチャーショック(?)

.NET6(というかC#10?)からは、Main関数を書かなくてもそのままいきなりコードを書くことができるようになりました

なんかちょっと別の言語っぽさがありますが、ちゃんと使えます
このエントリポイントとなっているファイル？は `async` が効いているようで、 `await` キーワードを入れても怒られずにそのまま使えます

```Program.cs
// See https://aka.ms/new-console-template for more information

using JsonTest;

await Section1.Exec();

await Section2.ExecWithNewtonsoftJson();
await Section2.ExecWithSystemTextJson();
```

ここで出てくる `Section1.Exec()` とかは後ほど記述いたします

# Section1: .NET6 ではjsonはデシリアライズしなくてもデータを取得できる

本セクションでは、気象庁お天気APIもどきを通して取得したjsonを `System.Text.Json` でパースする方法について記述致します

## 気象庁お天気APIもどき について

下記の本解説のコードでも記載しておりますが、東京都のお天気情報のjsonはこちらに配置されております

https://www.jma.go.jp/bosai/forecast/data/forecast/130000.json

取得できる情報はおおむねこんな感じになっています

```json
[
  {
    "publishingOffice": "気象庁",
    "reportDatetime": "2021-12-02T17:00:00+09:00",
    "timeSeries": [
      {
        "timeDefines": [
          "2021-12-02T17:00:00+09:00",
          "2021-12-03T00:00:00+09:00",
          "2021-12-04T00:00:00+09:00"
        ],
    :
    :
  }
  :
  :
]
```

## 本解説

.NET6では、受け取ったjsonをそのまま `JsonNode.Parse(...)` に渡すことで、 `JsonNode` クラスのインスタンスを生成することができます
この `JsonNode` は json のオブジェクト構造そのものとなっていて、配列を扱うようにオブジェクトの中の要素にアクセスすることができます

下記の例では気象庁のAPIもどきから東京都の今日、明日、明後日のお天気情報をjsonで取得し、そこから `publishingOffice` (情報の出どころ) の情報を取得しています

```Section1.cs
using System;
using System.Text.Json.Nodes;

namespace JsonTest
{
    public class Section1
    {
        public static async Task Exec()
        {
            // 気象庁のAPIもどきから東京都のお天気情報をjsonでもらう
            var client = new HttpClient();
            var response = await client.GetAsync(@"https://www.jma.go.jp/bosai/forecast/data/forecast/130000.json");

            var json = await response.Content.ReadAsStringAsync();

            // ここで System.Text.JsonでおもむろにJsonNodeにパース
            var jsonNode = JsonNode.Parse(json);

            // すると、こんな感じでアクセスして値を引っ張ってくることができる
            var publishingOffice = jsonNode[0]["publishingOffice"]?.ToString();

            Console.WriteLine($"publishingOffice: {publishingOffice}");
        }
    }
}

```

この気象庁のお天気APIもどきの場合、jsonの内容はいきなり配列 (`[]`) から始まっておりますが、
そんな場合でも、 `jsonNode[0]` というような感じで、配列を扱うかのようにアクセスして値を取得することができます

```csharp
// ここで System.Text.JsonでおもむろにJsonNodeにパース
var jsonNode = JsonNode.Parse(json);

// すると、こんな感じでアクセスして値を引っ張ってくることができる
var publishingOffice = jsonNode[0]["publishingOffice"]?.ToString();
```

# Section2: 実際に `Newtonsoft.Json` を使った場合と比較してみたら？

では、既存のJsonパーサと記述を比べてみたらどうでしょうか？
こんな感じで書き比べができます

```Section2.cs
using System;
using System.Text.Json.Nodes;
using Newtonsoft.Json;

namespace JsonTest
{
    public class Section2
    {
        private static string _json = "";

        /// <summary>
        /// 気象庁のお天気APIもどきから東京都の今日、明日、明後日のお天気情報をjsonで取ってくる
        /// </summary>
        /// <returns>json文字列</returns>
        private static async Task<string> FetchWeatherFromJMA()
        {
            if (string.IsNullOrWhiteSpace(_json))
            {
                var client = new HttpClient();
                var response = await client.GetAsync(@"https://www.jma.go.jp/bosai/forecast/data/forecast/130000.json");

                _json = await response.Content.ReadAsStringAsync();
            }

            return _json;
        }

        /// <summary>
        /// お天気情報jsonをNewtonsoft.Jsonでパースしてみる
        /// </summary>
        /// <returns>Task(await用)</returns>
        public static async Task ExecWithNewtonsoftJson()
        {
            var json = await FetchWeatherFromJMA();
            var weatherModelList = WeatherModel.FromJson(json);

            Console.WriteLine("ExecWithNewtonsoftJson()");
            foreach (var m in weatherModelList)
            {
                var area = m.TimeSeries.First().Areas.First();
                Console.WriteLine(m.ReportDatetime.ToString());
                Console.WriteLine(area.Area.Name);

                if (area.Weathers == null)
                {
                    continue;
                }

                foreach (var w in area.Weathers)
                {
                    Console.WriteLine(w);
                }
            }
        }

        /// <summary>
        /// お天気情報jsonをSystem.Text.Jsonでパースしてみる
        /// </summary>
        /// <returns>Task(await用)</returns>
        public static async Task ExecWithSystemTextJson()
        {
            var json = await FetchWeatherFromJMA();
            var jsonNode = JsonNode.Parse(json);

            if (jsonNode == null)
            {
                Console.WriteLine("jsonNode is null");
                return;
            }

            Console.WriteLine("ExecWithSystemTextJson()");
            var jsonArray = jsonNode.AsArray();
            Console.WriteLine(jsonArray.Count);
            foreach (var m in jsonArray)
            {
                var area = m["timeSeries"]?.AsArray().FirstOrDefault()["areas"]?.AsArray().FirstOrDefault();
                Console.WriteLine(m["reportDatetime"]?.ToString() ?? string.Empty);
                Console.WriteLine(area["area"]?["name"] ?? string.Empty);

                if (area["weathers"] == null)
                {
                    continue;
                }

                foreach (var w in area["weathers"].AsArray())
                {
                    Console.WriteLine(w.ToString());
                }
            }
        }
    }
}
```

行数はほぼ同じような感じで書けますが、 `System.Text.Json` の方がちょっとゴチャついている感はありますね
しかし、 `System.Text.Json` の方は `First()/FirstOrDefault()` が使用できる、つまりLINQが使えます
そして、 `Newtonsoft.Json` の方については以下のおまけ(モデルクラスの作成)がついてきます

※この記事上に記載しようと一瞬考えましたが、モデルクラスだけで250行を超えました、本記事の趣旨とは外れますので割愛します
※実際の記述はこのURLをご覧ください => https://github.com/cocoalix/QiitaAdvCal2021Dec03DotNet6/blob/master/JsonTest/WeatherModel.cs

# とりあえずデシリアライズで使ってみたけど、メリットとデメリットは？

## メリット(うれしいこと)

- とりあえずjsonの形していればパースできてそのまま値ひっぱってこれるのいいよね
    - てことはアクセス先のAPIのjsonの構造がふわふわしている時に使うとちょうどいいかもしれませんね
- jsonの構造が決まっていても、場合によって文字列だったり数値だったりあやふやしているときにもとりあえずアクセスできるのもいいよね
- 急にjsonの構造が変わっても、作り方によってはエラーでアプリが落ちるみたいなことになりづらいかもしれない

## デメリット(よくないかなー？のところ)

- とりあえずjsonのオブジェクト構造にアクセスできるけど、代わりにIntelliSenseの支援が受けられない
- パースがうまくできなかったことをきちんとエラーとして落としたい時には不向きかもしれない
- モデルクラス作るのがコストみたいに筆者は言ってるけど、jsonのデータからモデルクラスを自動生成してくれるWebサービスがあるから別にコストじゃなくない？
    - ごもっともー！！

# ところで

今回はjsonからオブジェクトへのデシリアライズにのみフォーカスしましたが、もしかしたらリクエストに使用するjsonをモデルクラスなしに生成できることの方が強いかもしれません
(今回は執筆期間の関係上取り上げることが叶いませんが、よろしければご覧くださいませ)

https://docs.microsoft.com/ja-jp/dotnet/standard/serialization/system-text-json-use-dom-utf8jsonreader-utf8jsonwriter?pivots=dotnet-6-0#create-a-jsonnode-dom-with-object-initializers-and-make-changes

# さいごに

このような感じで、 `System.Text.Json` も改善されていることが確認できました
今回の使用感だとプロトタイピング？に向いているような感じを筆者は感じ取りました

いろんな言語やフレームワークがバージョンアップを重ねるごとに機能を追加/改善しておりますが、それは.NET6も例外ではありません、素晴らしいですね

何か誤っている個所がありましたらご指摘ください
ここまで読んでくださり、ありがとうございました

## あしたは

明日 2021年12月04日 の本カレンダーの記事は @kaorumori さんの「Blazor WebAssemblyのAOTコンパイルによる高速化とAzure Static Web Appsへのデプロイ」となります

https://qiita.com/kaorumori/items/88d836fab1fcbbbb7114

BlazorのWebAssemblyの方は私は使ったことがございませんので、非常に楽しみです

引き続き、Qiita AdventCalendar2021を、　`.NET6` での開発を楽しんでいきましょう！