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.DeserializeObjectにDictionary<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
また例外です。今度は LinkedTreeMap を Nanika にキャストできなかったという内容です。これはAndroid Studioで変数の中身をのぞくとわかるのですが LinkedTreeMap<String, ArrayList<LinkedTreeMap>> になってます。なんでやねん。
結局どうするのか
デシリアライズ先の型のネストされたジェネリック型を 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>>にすれば受け取れます
