かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

JSON.NETで変なJSONを読み込む方法

Pocketのクライアントを作ろうとJSONを読み込んでたんですよ。JSON.NETで。 んで、配列で結果返すというやつがあったので正直に配列で受け取ろうとしたら…

{
  values: 
  {
    "1": {"name": "taro"},
    "2": {"name": "jiro"},
    "3": {"name": "saburo"}
  }
}

こんなJSON返してきやがった。Arrayじゃない!Objectです。ということで、以下のようにデータを受け取るようにしました。

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

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            var json = @"
{
  values: 
  {
    ""1"": {""name"": ""taro""},
    ""2"": {""name"": ""jiro""},
    ""3"": {""name"": ""saburo""}
  }
}
";
            var item = JsonConvert.DeserializeObject<Item>(json);
            foreach (var v in item.Values)
            {
                Console.WriteLine("{0}: {1}", v.Key, v.Value.Name);
            }
        }
    }

    class Item
    {
        // 何がくるかわからなならDictionaryしか…
        [JsonProperty("values")]
        public Dictionary<string, Person> Values { get; set; }
    }
    class Person
    {
        [JsonProperty("name")]
        public string Name { get; set; }
    }
}

リストじゃないと思いつつ、これで動いたので進めようと思ったら、データが0件のときに以下のようなJSONが返ってきました。

{
  values:[]
}

あ…うん知ってる。それ配列…。

これをさっきのプログラムに食わせると…

ハンドルされていない例外: Newtonsoft.Json.JsonSerializationException: Cannot des
erialize the current JSON array (e.g. [1,2,3]) into type 'System.Collections.Gen
eric.Dictionary`2[System.String,ConsoleApplication1.Person]' because the type re
quires a JSON object (e.g. {"name":"value"}) to deserialize correctly.
To fix this error either change the JSON to a JSON object (e.g. {"name":"value"}
) or change the deserialized type to an array or a type that implements a collec
tion interface (e.g. ICollection, IList) like List<T> that can be deserialized f
rom a JSON array. JsonArrayAttribute can also be added to the type to force it t
o deserialize from a JSON array.

はい。おっしゃる通りです。

なので、いったんdynamicで受けてTypeプロパティで型を見てからつどシリアライズしてでシリアライズする(超非効率)

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

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
//            var json = @"
//{
//  values: 
//  {
//    ""1"": {""name"": ""taro""},
//    ""2"": {""name"": ""jiro""},
//    ""3"": {""name"": ""saburo""}
//  }
//}
//";
            var json = @"
{
  values:[]
}
";
            var item = JsonConvert.DeserializeObject<Item>(json);
            foreach (var v in item.GetValues())
            {
                Console.WriteLine("{0}: {1}", v.Key, v.Value.Name);
            }
        }
    }

    class Item
    {
        // いったんdynamicで受けておいて
        [JsonProperty("values")]
        public dynamic Values { get; set; }

        public Dictionary<string, Person> GetValues()
        {
            // 中身の型を見て再度必要な型になおす
            if (this.Values.Type == JTokenType.Object)
            {
                var json = JsonConvert.SerializeObject(this.Values);
                return JsonConvert.DeserializeObject<Dictionary<string, Person>>(json);
            }

            // オブジェクトじゃなかったら空だよね
            return new Dictionary<string, Person>();
        }
    }
    class Person
    {
        [JsonProperty("name")]
        public string Name { get; set; }
    }
}

とりあえず、これで逃げてるけど、もっといい方法ないのかなぁ。実際にはGetValuesにあたるメソッドは、実際には二度目からはキャッシュした値を返すようにしてます。