11

I'm writing some code that is going to be used to retrieve resources from a website. It all sort of looks like this:

public Collection<Project> getProjects() {
        String json = getJsonData(methods.get(Project.class)); //Gets a json list, ie [1, 2, 3, 4]
        Gson gson = new Gson();
        Type collectionType = new TypeToken<Collection<Project>>() {}.getType();
        return gson.fromJson(json, collectionType);
    }

So naturally I tried abstracting it using Java generics.

/*
     * Deserialize a json list and return a collection of the given type.
     * 
     * Example usage: getData(AccountQuota.class) -> Collection<AccountQuota>
     */
    @SuppressWarnings("unchecked")
    public <T> Collection<T> getData(Class<T> cls) {
        String json = getJsonData(methods.get(cls)); //Gets a json list, ie [1, 2, 3, 4]
        Gson gson = new Gson();
        Type collectionType = new TypeToken<Collection<T>>(){}.getType();
        return (Collection<T>) gson.fromJson(json, collectionType);
}

The generic version of the code doesn't quite work though.

public void testGetItemFromGetData() throws UserLoginError, ServerLoginError {
            Map<String,String> userData = GobblerAuthenticator.authenticate("foo@example.com", "mypassword");
            String client_key = userData.get("client_key");
            GobblerClient gobblerClient = new GobblerClient(client_key);
            ArrayList<Project> machines = new ArrayList<Project>();
            machines.addAll(gobblerClient.getData(Project.class));
            assertTrue(machines.get(0).getClass() == Project.class);
            Log.i("Machine", gobblerClient.getData(Project.class).toString());
        }


java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to com.gobbler.synchronization.Machine
at com.gobblertest.GobblerClientTest.testGetItemFromGetData(GobblerClientTest.java:53)
at java.lang.reflect.Method.invokeNative(Native Method)
at android.test.AndroidTestRunner.runTest(AndroidTestRunner.java:169)
at android.test.AndroidTestRunner.runTest(AndroidTestRunner.java:154)
at android.test.InstrumentationTestRunner.onStart(InstrumentationTestRunner.java:545)
at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:1551)

The class in question:

import java.util.Map;


public class Project {
    private int total_bytes_stored;
    private String name;
    private String user_data_guid;
    private int seqnum;
    private String guid;
    private Map current_checkpoint;
    private Map<String, String> upload_folder;
    // TODO: schema_version
    private boolean deleted;
    // TODO: download_folders

    public Project() {} // No args constructor used for GSON
}

I'm not quite familiar with all the details of Java generics or GSON internals and my search has not been particularly informative. There a bunch of questions here on SO, but most refer to implementing methods like the original one I had. And the GSON docs don't seem to cover this particular case. So again, how can I use Google GSON to deserialize a JSON array into a a Collection of a generic type?

Ceasar
  • 22,185
  • 15
  • 64
  • 83

4 Answers4

34

I think you can! From the Gson user guide:

You can solve this problem by specifying the correct parameterized type for your generic type. You can do this by using the TypeToken class.

This means that you could in theory, wrap your Map like so:

Type t = new TypeToken<Map<String,Machine>>() {}.getType();
Map<String,Machine> map = (Map<String,Machine>) new Gson().fromJson("json", t);

Or (in my case) I had to wrap a List like so:

Type t = new TypeToken<List<SearchResult>>() {}.getType();
List<SearchResult> list = (List<SearchResult>) new Gson().fromJson("json", t);

for (SearchResult r : list) {
  System.out.println(r);
}

(I was getting the exception "java.lang.ClassCastException: com.google.gson.internal.StringMap cannot be cast to my.SearchResult".)

jevon
  • 3,197
  • 3
  • 32
  • 40
  • Yes, It's works fine for me. But in the obfuscation using Proguard of the above code, Getting "anonymous class(List) not found". Is I am missing something? Else have to change or add code for keep the anonymous class in proguard config file.? – Karthick Jun 27 '13 at 07:22
8

Sadly, you can't. The generic type has been erased by the compiler, and there's no way for Gson to know what types you have or what their fields are.

Jesse Wilson
  • 39,078
  • 8
  • 121
  • 128
  • Good to hear, if just to give closure. I understand the topic is probably a bit involved, but can you explain why briefly? – Ceasar Apr 28 '12 at 06:02
  • 1
    For backwards compatibility and implementation simplicity the JVM doesn't know the types behind type parameters at runtime. Usually you don't care, but this is one of the situations where you do. The web has a lot of articles about "Java erasure" that have more details. – Jesse Wilson Apr 29 '12 at 22:09
  • Actually you can, check answer given by @jevon http://stackoverflow.com/a/12576189/608805 – raukodraug Sep 09 '14 at 21:57
5

I used JsonReader to deserialize json string as follow.

public class JSONReader<T> {

    .....

    private Class<T> persistentClass;

    public Class<T> getPersistentClass() {
        if (persistentClass == null) {
            this.persistentClass = (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
        }
        return persistentClass;
    }

    public List<T> read(Reader reader) throws IOException {
        JsonReader jsonReader = new JsonReader(reader);
        List<T> objs = new ArrayList<T>();
        jsonReader.beginArray();      
        while (jsonReader.hasNext()) {
            T obj = (new Gson()).fromJson(jsonReader, getPersistentClass());
            if (logger.isFine()) {
                logger.fine(obj.toString());
            }
            objs.add(obj);
        }
        jsonReader.endArray();
        jsonReader.close();
        return objs;
    }

    .....
}

You can call above method use below statements (convert Json string to StringReader, and pass StringReader object into the method):

public class Test extends JSONReader<MyClass> {

    .....

    public List<MyClass> parse(String jsonString) throws IOException {
        StringReader strReader = new StringReader(jsonString);
        List<MyClass> objs = read(strReader);
        return objs;
    }

    .....
}

Hopefully can works!

Robertus
  • 51
  • 1
  • 2
1

The most generic type I can think of in Java is a Map<String, Object> to represent a JSON object and List<Map<String, Object>> to represent an array of JSON objects.

It is very straight forward to parse this using the GSON library (I used version 2.2.4 at the time of writing):

import com.google.gson.Gson;

import java.util.List;
import java.util.Map;

public class Test {
    public static void main(String[] args) {
        String json = "{\"name\":\"a name\"}";
        Gson GSON = new Gson();
        Map<String, Object> parsed = GSON.fromJson(json, Map.class);
        System.out.println(parsed.get("name"));

        String jsonList = "[{\"name\":\"a name\"}, {\"name\":\"a name2\"}]";
        List<Map<String, Object>> parsedList = GSON.fromJson(jsonList, List.class);
        for (Map<String, Object> parsedItem : parsedList) {
            System.out.println(parsedItem.get("name"));
        }
    }
}
leonardseymore
  • 533
  • 5
  • 20