ログイン中のQiita Team
ログイン中のチームがありません

Qiita Team にログイン
コミュニティ
OrganizationイベントアドベントカレンダーQiitadon (β)
サービス
Qiita JobsQiita ZineQiita Blog
C#
JSON
System.Text.Json
Newtonsoft.Json
27
どのような問題がありますか?

この記事は最終更新日から1年以上が経過しています。

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

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

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

Microsoft公式のSystem.Text.Jsonを使ってJSONを扱うためには、先に展開したいJSONと相似形のクラスを用意しておく必要があります。

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

    // 展開する先のクラス構造
    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はかなり機能が少ないです。
このような用途にはNewtonsoft.Jsonのほうが向いています。

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

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

…普通にNewtonsoft.Json公式にあったわ。

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

やったか!
と思いきやDatesAvailableあたりはstringじゃないので当然ながら死にます。
パース方法はDataSetとかImmutableListとか色々ありますが、どれも全ての値が同じ型で、段階がフラットであることを前提としたものです。

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

それからなんやかんやあって、最終的にJObjectを使ってどうにかすることができました。

    // デシリアライズ
    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ならどれも一瞬でできるのに……とか思いつつここまで調べるのに半日かかった。

なおサンプルはそのままだと.ToList()のあたりでエラーになるので、どこかで仕様が変わったようです。

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

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

ユーザー登録して、Qiitaをもっと便利に使ってみませんか。
  1. あなたにマッチした記事をお届けします
    ユーザーやタグをフォローすることで、あなたが興味を持つ技術分野の情報をまとめてキャッチアップできます
  2. 便利な情報をあとで効率的に読み返せます
    気に入った記事を「ストック」することで、あとからすぐに検索できます
rana_kualu
不労所得で生きたい。
この記事は以下の記事からリンクされています

コメント

リンクをコピー
このコメントを報告

不定形のJSONとは言っても、形式が決まっていないならばプログラムで書くことは不可能なのでclassを用意できるはずというのが流儀です。
例示のサンプルならば"TemperatureRanges"という名前が決まっているはずなので、それに合わせてclassを作れば良いはずです。

どうしてもclassを用意したくなければdynamicを使うのが良いと思います。

0
リンクをコピー
このコメントを報告

Newtonsoft.Jsonのサンプルでは、LINQ to JSONの項を参考にすると望みの機能があると思います。

0
(編集済み)
リンクをコピー
このコメントを報告

JObjectはIDictionaryを実装しているので、{}のリードは以下のように書いた方がすっきりします。

// {}
// 型がわかるようにKeyValuePair<string, JToken>にしたけど、普通はvarで十分
foreach (KeyValuePair<string, JToken> TemperatureRange in weatherForecast["TemperatureRanges"])
{
    // キー
    Console.WriteLine("TemperatureRange Key: " + TemperatureRange.Key);
    // 値
    Console.WriteLine("TemperatureRange Value.High: " + TemperatureRange.Value["High"];
    // 一部だけ定型クラスに取り出したい
    switch(TemperatureRange.Key)
        case "Hot":
            HighLowTemps highLowTemps = TemperatureRange.Value.ToObject<HighLowTemps>();
            break;
    }
}

Newtonsoft.JsonのJToken(の派生クラスたち)はJSONをDOMや標準ライブラリのSystem.Xml.XmlNodeのように扱えるようにうまく設計されていますね。
そしてJTokenはIDynamicMetaObjectProviderを実装しているので、dynamicオブジェクトとしても扱えるっぽいです。(具体的なやり方はわからない)

0
どのような問題がありますか?
あなたもコメントしてみませんか :)
ユーザー登録
すでにアカウントを持っている方はログイン
記事投稿イベント開催中
Azure Kubernetes Serviceに関する記事を投稿しよう!
~
27
どのような問題がありますか?
ユーザー登録して、Qiitaをもっと便利に使ってみませんか

この機能を利用するにはログインする必要があります。ログインするとさらに下記の機能が使えます。

  1. ユーザーやタグのフォロー機能であなたにマッチした記事をお届け
  2. ストック機能で便利な情報を後から効率的に読み返せる
ユーザー登録ログイン
ストックするカテゴリー