Java Type Generic as Argument for Gson

Using a generic type with Gson

The simplest approach for what you are attempting is to define the type of class which is to be returned in the method signature itself. You can do so by either passing an instance of 'T' to the method or the Class of the value to be returned. The second approach is the more typical in cases where you expect to generate a return value. Here is an example of this approach using Gson:

public <T> T deserialize(String jsonString, Class<T> clazz) {
GsonBuilder builder = new GsonBuilder();
builder.setDateFormat("MM/dd/yy HH:mm:ss");

Gson gson = builder.create();
return gson.fromJson(jsonString, clazz);
}

Usage:

MyClass mc = deserialize(jsonString, MyClass.class);

Deserializing Generic Types with GSON

You have to specify the type of T at the time of deserialization. How would your List of posts get created if Gson didn't know what Type to instantiate? It can't stay T forever. So, you would provide the type T as a Class parameter.

Now assuming, the type of posts was String you would deserialize MyJson<String> as (I've also added a String json parameter for simplicity; you would read from your reader as before):

doInBackground(String.class, "{posts: [\"article 1\", \"article 2\"]}");

protected MyJson<T> doInBackground(Class<T> type, String json, Void... params) {

GsonBuilder gson = new GsonBuilder();
Type collectionType = new TypeToken<MyJson<T>>(){}.getType();

MyJson<T> myJson = gson.create().fromJson(json, collectionType);

System.out.println(myJson.getPosts()); // ["article 1", "article 2"]
return myJson;
}

Similarly, to deserialize a MyJson of Boolean objects

doInBackground(Boolean.class, "{posts: [true, false]}");

protected MyJson<T> doInBackground(Class<T> type, String json, Void... params) {

GsonBuilder gson = new GsonBuilder();
Type collectionType = new TypeToken<MyJson<T>>(){}.getType();

MyJson<T> myJson = gson.create().fromJson(json, collectionType);

System.out.println(myJson.getPosts()); // [true, false]
return myJson;
}

I've assumed MyJson<T> for my examples to be as

public class MyJson<T> {

public List<T> posts;

public List<T> getPosts() {
return posts;
}
}

So, if you were looking for to deserialize a List<MyObject> you would invoke the method as

// assuming no Void parameters were required
MyJson<MyObject> myJson = doInBackground(MyObject.class);

Use Gson library with a generic class as a field of the class to serialize

You can't do this without somehow knowing the type T at compile time in some manner due to Java's type erasure.

One option for this is the JSON can contain some information specifying the type, e.g.

{
"sendDataType": "MyRequest",
"sendData": {
...
}
}

If you then make SendData generic e.g.

SendData<T> { 
private SendDataRequestObject<T> sendData;
}

you can then parse the JSON once to find out the sendDataType:

SendData<?> genericSendData = new GsonBuilder().create().toJson(requestObject, new TypeToken<SendData<?>>(){});
String sendDataType = genericSendData.sendDataType;

and use that to create a TypeToken of the right type:

switch(sendDataType) {
case "MyRequest":
return new TypeToken<MyRequest>(){};
}

And then parse the JSON again specifying the generic type now that you know it:

SendData<?> myRequestSendData = new GsonBuilder().create().toJson(requestObject, typeToken);

This works because our switch statement knows the possible types at compile time and can create TypeTokens for them.

GSON using Java generics

Because of type erasure ValidationResult and ValidationResult<User> are the same type. There will only ever be one Class object for that type. Because Gson only knows about ValidationResult through that Class object, it has to use a default for the generic type. In this case, it uses a LinkedHashMap.

The easiest solution is to provide a TypeToken, which is kind of a hack to get around type erasure (but not really). You can use it like so

gson.fromJson(json, new TypeToken<ValidationResult<User>>() {}.getType());

Through the Type, Gson has access to the generic type argument. Note how I've hardcoded the type User as an argument. You have to use an actual type here for Gson to get the actual type. If you use a generic type parameter, Gson will get a generic type parameter as a value and again will not know how to use it. I explain why here.

Another solution is to create a subtype of ValidationResult like

public class UserValidationResult extends ValidationResult<User> {}

and pass the Class object of that to your method.

Gson Parsing Generic List From Reflected Field Type

This happens because of how Java generics are implemented: <T> can only exist in compiler's mind or in class file metadata. Let's take a closer look and perform a brief analysis:

@SuppressWarnings("unchecked")
public static <T> void getListFromJson(Method m, Object target, String json, Class<T> elementType) throws Exception {
m.invoke(target, (List<T>)gson.fromJson(json, new TypeToken<List<T>>(){}.getType()));
}
  • Class<T> elementType does not play here at all because of that Java generics restrictions. It's just an attempt to put some generic restrictions for the method, but if you'd remove the parameter, nothing would ever change. Moreover, Class<T> can hold raw types only, and even if you use unchecked casts to make Class<T> hold something like List<List<List<String>>>, you'll still get the top-most raw type List, and you could remove the parameter (however, hold it for a scenario described below).
  • Type tokens are based on the fact that JVM allows to store the super class parameterization. You might notice that type tokens are parameterized with type parameters known at compile time: new TypeToken<String> or new TypeToken<List<Integer>> -- in this case compiler can generate proper superlcass parameterization based on known types. These types are used in type tokens. <T> is unknown at compile time. Check it in runtime: System.out.println(new TypeToken<T>() {}.getType()); -- the output is T. What is T? Just a wildcard, not a real type.
  • You have to understand what type tokens are for: they are just a convenient way of what could Java support if there were something like List<Integer>.type (illegal and not .class!). Type tokens just analyze their parameterizations are return Type instances that may be ParameterizedType instances. Moreover, you can create your own ParameterizedType without type tokens, because these are just interfaces that define some getters. So, in order to build type tokens dynamically:

    • if you're using Gson 2.8.0, use TypeToken.getParameterized(List.class, elementType).getType();
    • if you have an older Gson version or you want control it as much as possible, just create and return a ParameterizedType instance yourself:
new ParameterizedType() {
@Override public Type getRawType() { return List.class; }
@Override public Type[] getActualTypeArguments() { return new Type[]{ elementType }; }
@Override public Type getOwnerType() { return null; }
}

Pretty self-descriptive, isn't it?

  • Why does Gson returns Doubles rather than Longs? Having a wildcard of <T>, Gson uses the default strategy as if the type was completely unknown: doubles are used by default for all numbers since they can keep all standard Java number values. And that's why you get the cast exception when you're dereferencing a list element only (but not lists -- type parameters do not exist for local variables.)

Sorry for such a vague explanation, but summarizing it all up, you only need the following method:

static <T> void getListFromJson(final Method method, final Object target, final String json, final Type elementType)
throws InvocationTargetException, IllegalAccessException {
final List<T> ts = gson.fromJson(json, TypeToken.getParameterized(List.class, elementType).getType());
method.invoke(target, ts);
}

Note that now the elementType parameter plays the game, the type tokens are used to create a ParameterizedType instance, and Gson now can use deserializations strategy for the type defined with elementType (Long in that place).

Output:

class java.lang.Long

[52881]

52881

Specifying generic type information through a parameter

This isn't going to be possible using a Class<T> argument, since Class only supports representing raw types like List - the type List<GameInfo> cannot be represented by a Class object, which is why TypeToken exists.

Your method would need to take a TypeToken<T> argument instead and leave it up to the caller to create that argument:

private <T extends Collection<U>, U> T collectionFromJson(String pResponseJson, TypeToken<T> typeToken) {
return (T)new Gson().fromJson(pResponseJson, typeToken.getType());
}

...

TypeToken<List<GameInfo>> typeToken = new TypeToken<List<GameInfo>>() { };
List<GameInfo> lst = collectionFromJson(response, typeToken);

(disclaimer: I only have experience with Java/generics, not GSON)



Related Topics



Leave a reply



Submit