Strange behavior when deserializing nested, generic classes with GSON

10,436

Solution 1

The short answer is that you need to move the creation of the TypeToken out of Executor, bind the T in Response<T> when you create the token (new TypeToken<Response<Language>>() {}), and pass in the type token to the Executor constructor.

The long answer is:

Generics on a type are typically erased at runtime, except when the type is compiled with the generic parameter bound. In that case, the compiler inserts the generic type information into the compiled class. In other cases, that is not possible.

So for instance, consider:

List<Integer> foo = new ArrayList<Integer>();

class IntegerList extends ArrayList<Integer> { ... }
List<Integer> bar = new IntegerList();

At runtime, Java knows bar contains integers because the Integer type is bound to ArrayList at compile time, so the generic type information is saved in the IntegerList class file. However, the generic type information for foo is erased, so at runtime it is not really possible to determine that foo is supposed to contain Integers.

So it often comes up that we need generic type information in a situation where it normally would be erased before runtime, such as here in the case of parsing JSON data in GSON. In these situations, we can take advantage of the fact that type information is preserved when it is bound at compile-time (as in the IntegerList example above) by using type tokens, which are really just tiny anonymous classes that conveniently store generic type information.

Now to your code:

Type responseType = new TypeToken<Response<T>>() {}.getType();

In this line of your Executor class, we create an anonymous class (inheriting from TypeToken) which has the type Response<T> hard coded (bound) at compile-time. So at runtime, GSON is able to determine that you want an object of Response<T>. But it doesn't know what T is, because you didn't specify it at compile-time! So GSON cannot determine what type will be in the List of the Response object it creates, and it just creates a StringMap instead.

The moral of the story is that you need to specify that T at compile-time. If Executor is meant to be used generically, you probably need to create the type token outside of that class, in your client code. Something like:

class Executor<T> {

    private TypeToken<Response<T>> responseType;
    private Response<T> response;

    public Executor(TypeToken<Response<T>> responseType) {
        this.responseType = responseType;
    }

    public void execute() {
        this.response = new Gson().fromJson(json, responseType.getType());
    }

    public Response<T> getResponse() { return this.response; }

}

// client code:
Executor<Language> executor = new Executor<Language>(new TypeToken<Response<Language>>() { });
executor.execute();
List<Language> languages = executor.getResponse().getData();
System.out.println(languages.get(0).alias); // prints "be"

By the way, I did test the above on my machine.

Sorry if that was too long!

Solution 2

You haven't called response.execute(); after Executor<Language> executor = new Executor<Language>(); this statement. You can't utilize the java generics here, but you can get the same effect with the following code.

Response.java

import java.io.Serializable;
import java.util.List;

/**
 *
 * @author visruth
 */
public class Response<T> implements Serializable {

    private List<T> data = null;

    public List<T> getData() {
        return this.data;
    }

    public void setData(List<T> data) {
        this.data = data;
    }

}

Language.java

import java.io.Serializable;

/**
 *
 * @author visruth
 */
public class Language implements Serializable {
    private String alias;
    private String label;

    public String getAlias() {
        return alias;
    }

    public void setAlias(String alias) {
        this.alias = alias;
    }

    public String getLabel() {
        return label;
    }

    public void setLabel(String label) {
        this.label = label;
    }

}

Finally, the Executor.java

import com.google.gson.Gson;
import java.util.*;

/**
 *
 * @author visruth
 */
public class Executor<T> {
private Response<T> response;

    public Response<T> getResponse() {
        return response;
    }

    /**
     * @param args the command line arguments
     */
    public void executor() {
        //sample data
        Response<Language> response=new Response<Language>();
        Language lan1=new Language();
        lan1.setAlias("alias1");
        lan1.setLabel("label1");
        Language lan2=new Language();
        lan2.setAlias("alias2");
        lan2.setLabel("label2");
        List<Language> listOfLangauges=new ArrayList<Language>();
        listOfLangauges.add(lan1);
        listOfLangauges.add(lan2);
        response.setData(listOfLangauges);
        Gson gson=new Gson();
        String json = gson.toJson(response);

        System.out.println(json);
        Response<Language> jsonResponse = gson.fromJson(json, Response.class);
        List list=jsonResponse.getData();

        List<Language> langs=new ArrayList<Language>();            
        for(int i=0; i<list.size(); i++) {            
        Language lan=gson.fromJson(list.get(i).toString(), Language.class);
        langs.add(lan);
        //System.out.println(lan.getAlias());
        }
        Response<Language> responseMade=new Response<Language>();
        responseMade.setData(langs);
        this.response=(Response<T>) responseMade;

    }
}

You can test it as follows

Executor<Language> executor = new Executor<Language>();
executor.executor();
List<Language> data = executor.getResponse().getData();
for(Language langu: data) {
    System.out.println(langu.getAlias());
    System.out.println(langu.getLabel());
}
Share:
10,436
Zar
Author by

Zar

Updated on June 03, 2022

Comments

  • Zar
    Zar almost 2 years

    I'm writing a class which will connect to a server and based on some arguments, retrieve a json-string which will be parsed with GSON to the specified (via generics) class.

    A stripped down version of the class in charge looks like this:

    class Executor<T> {
    
        private Response<T> response;
    
        public void execute() {
            Type responseType = new TypeToken<Response<T>>() {}.getType();
            this.response = new Gson().fromJson(json, responseType);
        }
    
        public Response<T> getResponse() { return this.response; }
    
    }
    

    (the JSON-variable looks like this.)

    The class which stores the data once de-serialized looks like this:

    class Response<T> {
    
        private List<T> data = null;
    
        public List<T> getData() { return this.data; }
    
    }
    

    The class which the data is trying to be de-serialized to:

    public class Language {
        public String alias;
        public String label;
    }
    

    And the code which runs utilizes the classes above:

    Executor<Language> executor = new Executor<Language();
    List<Language> languages = executor.execute().getResponse().getData();
    System.out.println(languages.get(0).alias); // exception occurs here
    

    Which results in the following exception

    ClassCastException: com.google.gson.internal.StringMap cannot be cast to sunnerberg.skolbibliotek.book.Language

    Any help or suggestions are greatly appreciated!

  • Zar
    Zar over 11 years
    Hmm, okay. Is there any way to overcome this? I guess I'm open for an alternate class structure, as long I don't have to create a new class for each Executor-pair.
  • Zar
    Zar over 11 years
    First of, I must say that StackOverflow's users never cease to amaze me. Wow, what an answer. Exactly what I was looking for (and needed!), many thanks to you! Your solution solved my problem perfectly. By the way, is there any reason why the JVM erases the type info?
  • Zar
    Zar over 11 years
    Thanks a lot for your answer, it gave me some (hopefully) good ideas!
  • matts
    matts over 11 years
    Well, thank you for giving a detailed and complete question! As for the type erasure, I believe Java does it this way to be backwards compatible. Compiled code that uses generics looks a lot like compiled code that doesn't use generics. So for example, if you have some old code that uses SomeLibrary 1.0 you could switch to SomeLibrary 2.0 (Now With Generics) without recompiling your code. Check out this blog post for another explanation.
  • Zar
    Zar over 11 years
    I guess it makes sense, thanks again! Interesting link, by the way.
  • Luciano Brum
    Luciano Brum almost 4 years
    What package is Response from?
  • Maksymilian Tomczyk
    Maksymilian Tomczyk over 3 years
    Thank you a lot, just finishing first day of playing with Kotlin and Java and your answer saved a lot of time :D