C#で不定形JSONを自在に扱いたい

C#でJSONを扱う記事は探せばたくさん出てくるのですが、そのほとんどが
・一段階のフラットJSONしか扱ってない
・先に展開先クラスを用意する方法
のどちらかしか扱っていません。

それらの方法では私のやりたいことができなかったので、そこらへんについて調べた記録です。


Microsoft公式の[System.Text.Json](https://docs.microsoft.com/ja-jp/dotnet/api/system.text.json)を使ってJSONを扱うためには、先に[展開したいJSONと相似形のクラスを用意しておく](https://docs.microsoft.com/ja-jp/dotnet/standard/serialization/system-text-json-how-to)必要があります。

以下では公式のJSONサンプルを例に、操作を行ってみます。

```cs
	// 展開する先のクラス構造
	public class WeatherForecastWithPOCOs
	{
	    public DateTimeOffset Date { get; set; }
	    public int TemperatureCelsius { get; set; }
	    public string Summary { get; set; }
	    public string SummaryField;
	    public IList<DateTimeOffset> DatesAvailable { get; set; }
	    public Dictionary<string, HighLowTemps> TemperatureRanges { get; set; }
	    public string[] SummaryWords { get; set; }
	}
	public class HighLowTemps
	{
	    public int High { get; set; }
	    public int Low { get; set; }
	}

	// JSON文字列の例
	string jsonString = "{\"Date\":\"2019-08-01T00:00:00-07:00\",\"TemperatureCelsius\":25,\"Summary\":\"Hot\",\"DatesAvailable\":[\"2019-08-01T00:00:00-07:00\",\"2019-08-02T00:00:00-07:00\"],\"TemperatureRanges\":{\"Cold\":{\"High\":20,\"Low\":-10},\"Hot\":{\"High\":60,\"Low\":20}},\"SummaryWords\":[\"Cool\",\"Windy\",\"Humid\"]}";

	// デシリアライズ
	var weatherForecast = JsonConvert.DeserializeObject<WeatherForecastWithPOCOs>(jsonString);
```

めんどくさすぎる…

何が困るって、APIとかの外部からやってくる不定形のJSONをパースしたいのですよ。
APIなんて形がすぐ変化するので、先に形式を用意しておかないといけないSystem.Text.Jsonは全くもってこの世界には向いていません。
もっとこう`$obj = json_decode($jsonString)`みたいなのはないんですかね。

実のところSystem.Text.Jsonは[かなり機能が少ない](https://docs.microsoft.com/ja-jp/dotnet/standard/serialization/system-text-json-migrate-from-newtonsoft-how-to)です。
このような用途には[Newtonsoft.Json](https://www.newtonsoft.com/json)のほうが向いています。


```cs
	// Newtonsoft.Jsonを使う
	var weatherForecast = JsonConvert.DeserializeObject(jsonString);
```

こうするとクラスを用意しておく必要がなくなるのですが、返ってくるのはobjectなのでいまいち役に立ちません。
全部Dictionaryあたりにしてほしいのですが。

…普通に[Newtonsoft.Json公式にあった](https://www.newtonsoft.com/json/help/html/DeserializeDictionary.htm)わ。

```cs
	Dictionary<string, string> weatherForecast = JsonConvert.DeserializeObject<Dictionary<string, string>>(jsonString);
```

やったか！
と思いきや`DatesAvailable`あたりはstringじゃないので当然ながら死にます。
パース方法は[DataSet](https://www.newtonsoft.com/json/help/html/DeserializeDataSet.htm)とか[ImmutableList](https://www.newtonsoft.com/json/help/html/DeserializeImmutableCollections.htm)とか色々ありますが、どれも全ての値が同じ型で、段階がフラットであることを前提としたものです。

中身が入れ子になってたりなってなかったりするJSONを、全部連想配列か何か展開してほしいのですよ。
`Dictionary<string, typeof(Value)>`みたいに書けないものですかね。

それからなんやかんやあって、最終的に[JObject](https://www.newtonsoft.com/json/help/html/SerializingJSONFragments.htm)を使ってどうにかすることができました。

```cs
    // デシリアライズ
    JObject weatherForecast = JObject.Parse(jsonString);

    // 直下の値を取得
    Console.WriteLine("Date: " + weatherForecast["Date"].ToString());

    // 入れ子の値を取得
    Console.WriteLine("TemperatureRanges_Cold_High: " + weatherForecast["TemperatureRanges"]["Cold"]["High"].ToString());

    // []
    var DatesAvailable = weatherForecast["DatesAvailable"].Children();
    foreach (var DatesAvailabl in DatesAvailable)
    {
        Console.WriteLine("DatesAvailable: " + DatesAvailabl.ToString());
    }

    // {}
    JEnumerable<JToken> TemperatureRanges = weatherForecast["TemperatureRanges"].Children();
    foreach (JToken TemperatureRange in TemperatureRanges)
    {
        // キー
        Console.WriteLine("TemperatureRange Key: " + ((JProperty)TemperatureRange).Name);
        // 値
        Console.WriteLine("TemperatureRange Value: " + TemperatureRange.First["High"]);
        // 一部だけ定型クラスに取り出したい
        HighLowTemps highLowTemps = TemperatureRange.First.ToObject<HighLowTemps>();
    }

    // トップレベルに追加
    weatherForecast["hoge"] = "fuga";

    // 削除
    weatherForecast.Property("Summary").Remove();

    // []に追加
    ((JArray)weatherForecast["DatesAvailable"]).Add("2020-02-02T02:02:02-02:00");

    // {}に追加
    ((JObject)weatherForecast["TemperatureRanges"]).Last.AddAfterSelf(
        new JProperty("Lukewarm", JObject.Parse(@"{""High"": 10,""Low"": 10}"))
    );
    // これでもいい
    weatherForecast["TemperatureRanges"]["Absolute"] = JObject.Parse(@"{""High"": -273,""Low"": -273}");

    // JSON文字列に戻す
    var jsonStringAfter = JsonConvert.SerializeObject(weatherForecast);
```


とりあえずこれで、C#でJSONをそこそこ自在に操ることができるようになったと思います。
PHPならどれも一瞬でできるのに……とか思いつつここまで調べるのに半日かかった。

なお[サンプル](https://www.newtonsoft.com/json/help/html/SerializingJSONFragments.htm)はそのままだと`.ToList()`のあたりでエラーになるので、どこかで仕様が変わったようです。

このくらいの記事ってPHPなら腐るほど転がってると思うのですが、C#だとMicrosoftのドキュメントか、ほとんど公式のコピペのような内容しか見つからないんですよね。
それらを応用してどうこうするって内容がどうにもなかなか存在しない。
独学でレベルアップするには非常に厳しい言語だと思います。

いや、この程度書くほどでもない常識だからどこにも書いてないんだよ、とか言われたら死にます。