11
11

More than 5 years have passed since last update.

Gsonでリストを持つジェネリックなMapをデシリアライズする

Posted at

Gsonで以下のようなJSONをMapと配列的なもののセット、つまりMap<String, Array<Nanika>>的なものにデシリアライズしたいという話です。なお、ここでは言語としてKotlinを使っています。

class Nanika(var value: String? = null) {}
{
   "0": [ { "value": "Hauhau" }, { "value": "Maumau" } ],
   "1": [ { "value": "Hello" }, { "value": "Konnichiwa" } ]
}

まとめ

  • Gsonに渡す型は TypeToken<T> を使ってジェネリックなパラメータ情報を受け渡す
  • デシリアライズの型は Map<String, Collection<T>> を使う (Collectionである必要がある, ArrayやListではだめ)
    • Arrayの場合は謎のException
    • Listの場合にはLinkedTreeMapになってしまう

C# (.NET) の場合

まず書き慣れたC#で書いてみます。大抵JSON.NETを使ってJsonConvert.DeserializeObjectDictionary<String, Nanika[]>を指定すれば出来上がりです。

class Nanika
{
    public string Value { get; set; }
}

void Main()
{
    var json = @"
    {
       ""0"": [ { ""value"": ""Hauhau"" }, { ""value"": ""Maumau"" } ],
       ""1"": [ { ""value"": ""Hello"" }, { ""value"": ""Konnichiwa"" } ]
    }
    ";

    JsonConvert.DeserializeObject<Dictionary<string, Nanika[]>>(json).Dump(); // Nanika[]
    JsonConvert.DeserializeObject<Dictionary<string, List<Nanika>>>(json).Dump(); // List<T> でも IList<T> でも OK
}

Gsonでデシリアライズするには

とりあえずまずは思いついたまま書いてみましょう。

GsonでデシリアライズするにはfromJsonメソッドを使います。fromJsonメソッドは第一引数にJSON文字列、第二引数にデシリアライズ後の型を受け取ります。

class Nanika(var value: String? = null) {}

val json = """
{
   "0": [ { "value": "Hauhau" }, { "value": "Maumau" } ],
   "1": [ { "value": "Hello" }, { "value": "Konnichiwa" } ]
}
"""
// Error: Only classes are allowed on the left hand side of a class literal
val retval = Gson().fromJson(json, Map<String, Array<Nanika>>::class.java)

まずコンパイルが通りません…。そもそもKotlinやJavaでC#で言うクローズジェネリックの型を直接引き出せないのです。
つまり List::class.java は書けても List<Nanika>::class.java は書けないということを意味します(多分この辺はType Erasureの影響の予感がします)。

Note: ちなみに List<Nanika>::class.java は書けませんが、配列である Array<Nanika>::class.java は書けます

ジェネリックな型を渡す

一旦もっとシンプルな Nanika 型のリスト的なものをデシリアライズしてみます。

val json = """
[ { "value": "Hello" }, { "value": "Konnichiwa" } ]
"""
// Error: Only classes are allowed on the left hand side of a class literal
val retval = Gson().fromJson(json, Array<Nanika>::class.java)

上記の通りこの書き方はできないのですが、Gsonには TypeToken<T> という型パラメータを保持したものを渡すための補助クラスがあるのでそれを利用します。

TypeToken<T> はそれを継承した無名クラスとインスタンスを作って、そこから type プロパティ(getTypeメソッド)を経由して型情報を取得できます。その型情報を fromJson メソッドに渡すことでGsonでデシリアライズ可能になります。

val json = """
[ { "value": "Hello" }, { "value": "Konnichiwa" } ]
"""
val typeToken = object : TypeToken<Array<Nanika>>() {} // protected なコンストラクタになっているので継承が必須
val retval = Gson().fromJson<Array<Nanika>>(json, typeToken.type) // => [ Nanika, Nanika ]

Note: ここでは Array<T> を使用しましたが、List<T> を指定しても同様にデシリアライズできます。

ということは

先ほどの TypeToken<T> を使って最初の例を書き換えてあげれば良さそうな気がします。

val json = """
{
   "0": [ { "value": "Hauhau" }, { "value": "Maumau" } ],
   "1": [ { "value": "Hello" }, { "value": "Konnichiwa" } ]
}
"""
val typeToken = object : TypeToken<Map<String, Array<Nanika>>>() {}
val retval = Gson().fromJson<Map<String, Array<Nanika>>>(json, typeToken.type)

こんな感じですね。そしてこれを実行するとどうなるかというと…

java.lang.RuntimeException: Unable to invoke no-args constructor for ? extends example.com.myapplication.Nanika[]. Register an InstanceCreator with Gson for this type may fix this problem.

なんと例外。よくわかりませんが Array<Nanika> (Nanika[] 型)をデシリアライズするときに失敗するようです。単純な配列の時はできていたのにも関わらずネストされたジェネリックな型の場合にはダメなようです。

それならば Array<T> の代わりに List<T> にしたらどうか?ということでこちらを試してみます。

val json = """
{
   "0": [ { "value": "Hauhau" }, { "value": "Maumau" } ],
   "1": [ { "value": "Hello" }, { "value": "Konnichiwa" } ]
}
"""
val typeToken = object : TypeToken<Map<String, List<Nanika>>>() {}
val retval = Gson().fromJson<Map<String, List<Nanika>>>(json, typeToken.type)

これを実行すると無事すんなりとデシリアライズするコードは通ります。

が、よかったよかった…とはならず、実際に retval["0"]!![0] などとして値を取り出そうとするとおかしいことに気づきます。

java.lang.ClassCastException: com.google.gson.internal.LinkedTreeMap cannot be cast to Nanika

また例外です。今度は LinkedTreeMapNanika にキャストできなかったという内容です。これはAndroid Studioで変数の中身をのぞくとわかるのですが LinkedTreeMap<String, ArrayList<LinkedTreeMap>> になってます。なんでやねん。

image.png

結局どうするのか

デシリアライズ先の型のネストされたジェネリック型を Array<T> でもなく List<T> でもなく ArrayList<T> でもなく Collection<T> にします。

val json = """
{
   "0": [ { "value": "Hauhau" }, { "value": "Maumau" } ],
   "1": [ { "value": "Hello" }, { "value": "Konnichiwa" } ]
}
"""
val typeToken = object : TypeToken<Map<String, Collection<Nanika>>>() {}
val retval = Gson().fromJson<Map<String, Collection<Nanika>>>(json, typeToken.type)

これで無事デシリアライズでき、内容を取り出せるようになります。

Note: ちなみに Collection<T> だとインデックスアクセスできないのですが、実際デシリアライズ後は ArrayList<T> なので fromJson の型パラメータを雑に Map<String, ArrayList<Nanika>> にすれば受け取れます

11
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
11