I noticed there wasn't an easy all-inclusive (POJO-free) way to convert (in GSON) a JsonArray
to a List
. I found an example that uses a TypeToken
and when I tried to use move this to a generic method (I'm a little hazy on my Java Generics), I found some odd behavior where JsonObject
and LinkedTreeMap
were confused. See example below:
public <T> List<T> jsonArrayToList(JsonArray array, Class<T> clazz) {
Gson GSON = new Gson();
Type listType = new TypeToken<List<T>>() {}.getType();
return GSON.fromJson(array,listType);
}
@Test
public void testGson() {
Gson GSON = new Gson();
String jsonString = "[ {key:\"foo\",value:\"bar\"}, {key:\"shoe\",value:\"car\"} ]";
JsonArray testArray = new JsonParser().parse(jsonString).getAsJsonArray();
Type listType = new TypeToken<List<JsonObject>>() {}.getType();
List<JsonObject> goodList = GSON.fromJson(testArray,listType);
List<JsonObject> badList = jsonArrayToList(testArray, JsonObject.class); // Actually a list of LinkedTreeMap?
System.out.println( goodList ); // [{"key":"foo","value":"bar"}, {"key":"shoe","value":"car"}]
System.out.println( badList ); // [{key=foo, value=bar}, {key=shoe, value=car}]
try {
long shoeCount = goodList.stream().filter(o -> o.get("key").getAsString().startsWith("sh")).count();
System.out.println( shoeCount ); // 1
} catch (Exception e) { System.out.println( e.getMessage() ); }
try {
long shoeCount = badList.stream().filter(o -> o.get("key").getAsString().startsWith("sh")).count();
System.out.println( shoeCount );
} catch (Exception e) {
System.out.println(e.getMessage()); // com.google.gson.internal.LinkedTreeMap cannot be cast to com.google.gson.JsonObject
}
}
I'm just curious why this occurs and/or if the generic method is incorrect.
CodePudding user response:
As explained in the comments, due to Java type erasure, the TypeToken<List<T>>
is actually a TypeToken<List<Object>>
at runtime, regardless of what value T
has. When Gson is told to deserialize as Object
it uses a Java type corresponding to the JSON data, for your JSON object that is java.util.Map
(with Gson's implementation LinkedTreeMap
).
In general you should therefore never use any type variables when creating a TypeToken
(unfortunately Gson itself does not prevent this usage).
For your specific use case you can use the method TypeToken.getParameterized
. With that you can create parameterized types with type arguments specified at runtime. This comes at the cost of missing type safety at compile time, so you have to make sure to provide the correct number of arguments to getParameterized
depending on the generic type (e.g. Map<K, V>
requires two type arguments) and that they do not validate the type bounds of the type variable.
Here is how you could use it in your method jsonArrayToList
:
public <T> List<T> jsonArrayToList(JsonArray array, Class<T> clazz) {
Gson GSON = new Gson();
Type listType = TypeToken.getParameterized(List.class, clazz).getType();
return GSON.fromJson(array,listType);
}