C# JSON文字列から不要な要素を削除する

JSONの扱いでちょっとした前処理が必要となりましたので、メモしておきます。

以下のようなフラットなJSON文字列を扱うケースがありました。

{
  "key1": "value1",
  "key2": "value2"
}

任意のキーと値(文字列型)が追加されるので、Dictionary型とした方が都合が良く、以下のようにJsonConvert.DeserializeObject<Dictionary<string, string>>(json)でデシリアライズさせていました。

using System;
using System.Collections.Generic;
using Newtonsoft.Json;

namespace ConsoleApplication
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var json = "{\"key1\":\"value1\",\"key2\":\"value2\"}";

            var dictionary = JsonConvert.DeserializeObject<Dictionary<string, string>>(json);

            foreach (var t in dictionary.Keys)
            {
                Console.WriteLine($"{t}: {dictionary[t]}");
            }
        }
    }
}

しかし業務要件の変更で、1つのキーのみ配列が渡されるようになりました。 (JSONでやり取りしている以上、当然そういうケースもあります。)

{
  "key1": "value1",
  "key2": "value2",
  "array1": [
    { "childkey1": "childvalue1" },
    { "childkey2": "childvalue2" }
  ]
}

フラットなkey-value構造が崩れるので、このままではパースできずにエラーとなってしまいます。

Unhandled Exception: Newtonsoft.Json.JsonReaderException: Unexpected character encountered while parsing value: [. Path 'array1', line 1, position 43.

これを回避するためには、パースできるクラスを自製してそのオブジェクトにデシリアライズする方法もありますが、もしarray1が必要無いのであれば、JSON文字列からarray1のキーと値を削除する方が楽です。

こうしたケースに便利なのがJObject(Newtonsoft.Json.Linq名前空間)で、JSONに対してLINQで処理できるようになります。

くだんの問題は、一旦JObjectとしてJSON文字列を前処理させることで解決できます。

  1. JSON文字列をJObjectへデシリアライズさせて、Remove(LINQ)でarray1を削除する
  2. 再度文字列にシリアライズしてから、Dictionary<string, string>へデシリアライズする
using Newtonsoft.Json.Linq;

var json = "{\"key1\":\"value1\",\"key2\":\"value2\",\"array1\":[{\"childkey1\":\"childvalue1\"},{\"childkey2\":\"childvalue2\"}]}";

var jobj = JObject.Parse(json);
jobj.Remove("array1");

var dictionary = JsonConvert.DeserializeObject<Dictionary<string, string>>(jobj.ToString());

キー名がわからずとにかく第1階層にある文字列型だけを取り出したいという場合も、JObjectはICollection<KeyValuePair<string, JToken>>を実装しているので、以下のようにLINQで処理できます。

jobj = JObject.Parse(json);

var dictionary = new Dictionary<string, string>();

foreach (var j in jobj)
{
    if (j.Value.Type == JTokenType.String)
    {
        dictionary.Add(j.Key, j.Value.ToString());
    }
}

// ToDictionary()を使えば1行で書けます
dictionary = jobj.Properties()
                .Where(jp => jp.Value.Type == JTokenType.String)
                .ToDictionary(jp => jp.Name, jp => jp.Value.ToString());

JSONで何らか前処理が必要になったときのツールとして持っておくと便利ですね。