3

The responses of a REST API always return a JSON with the following structure:

{
    "status": "<status_code>",
    "data": <data_object>
}

My problem is that the value of data doesn't have an unique type, but it can be a String, a JSON Object or a JSON Array, depending on the called endpoint. I can't figure out how to deserialize it in the right way to create the different Java objects...

For example, I've already prepared some POJOs: the root element

public class ApiResult {

    @SerializedName("status")
    public String status;

    @SerializedName("data")
    public JsonElement data;  // should I define it as a JsonElement??
}

and two objects that reflects two of the endpoints:

// "data" can be a list of NavItems
public class NavItem {

    @SerializedName("id")
    public String id;

    @SerializedName("name")
    public String name;

    @SerializedName("icon")
    public String icon;

    @SuppressWarnings("serial")
    public static class List extends ArrayList<NavItem> {}
}

and

// "data" can be a single object representing a Profile
public class Profile {

    @SerializedName("id")
    public String id;

    @SerializedName("fullname")
    public String fullname;

    @SerializedName("avatar")
    public String avatar;
}

Reading some StackOverflow questions, I've seen I should use the JsonDeserializer<T> interface. But how if the type of data in ApiResult is variable?

TheUnexpected
  • 3,077
  • 6
  • 32
  • 62

2 Answers2

5

You should use a a custom JsonDeserializer and write all your logic there, like this

ApiResult.java

public class ApiResult {

    @SerializedName("status")
    public String status;

    @SerializedName("data")
    public Object data; 
}

ApiResultDeserializer.java

import java.lang.reflect.Type;
import java.util.List;

import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;


public class ApiResultDeserializer implements JsonDeserializer<ApiResult> {

    private Type listType = new TypeToken<List<NavItem>>(){}.getType();

    @Override
    public ApiResult deserialize(JsonElement value, Type type,
            JsonDeserializationContext context) throws JsonParseException {

        final JsonObject apiResultJson = value.getAsJsonObject();

        final ApiResult result = new ApiResult();
        result.status = apiResultJson.get("status").getAsString();

        JsonElement dataJson = apiResultJson.get("data");

        if(dataJson.isJsonObject()) {
            result.data = context.deserialize(dataJson, NavItem.class);
        } else if(dataJson.isJsonPrimitive()) {
            result.data = context.deserialize(dataJson, String.class);
        } else if(dataJson.isJsonArray()) {
            result.data = context.deserialize(dataJson, listType);
        }

        return result;
    }

}

and try to create different kinds of data (List, Object, or String) as you mentioned

Main.java

Gson gson = new GsonBuilder()
        .registerTypeAdapter(ApiResult.class, new ApiResultDeserializer())
        .create();

        List<NavItem> navItems = new ArrayList<NavItem>();

        for(int i = 1 ; i < 6 ; ++i) {
            navItems.add(new NavItem(i+"", "Name-" + i, "Icon-" + i ));
        }

        ApiResult result = new ApiResult();
        result.status = "OK";
        result.data = navItems;

        // Serialization
        System.out.println(gson.toJson(result)); // {\"status\":\"OK\",\"data\":[{\"id\":\"1\",\"name\":\"Name-1\",\"icon\":\"Icon-1\"},{\"id\":\"2\",\"name\":\"Name-2\",\"icon\":\"Icon-2\"},{\"id\":\"3\",\"name\":\"Name-3\",\"icon\":\"Icon-3\"},{\"id\":\"4\",\"name\":\"Name-4\",\"icon\":\"Icon-4\"},{\"id\":\"5\",\"name\":\"Name-5\",\"icon\":\"Icon-5\"}]}

        result.data = navItems.get(0);

        System.out.println(gson.toJson(result)); // {\"status\":\"OK\",\"data\":{\"id\":\"1\",\"name\":\"Name-1\",\"icon\":\"Icon-1\"}}

        result.data = "Test";

        System.out.println(gson.toJson(result)); // {\"status\":\"OK\",\"data\":\"Test\"}


        // Deserialization
        String input = "{\"status\":\"OK\",\"data\":[{\"id\":\"1\",\"name\":\"Name-1\",\"icon\":\"Icon-1\"},{\"id\":\"2\",\"name\":\"Name-2\",\"icon\":\"Icon-2\"},{\"id\":\"3\",\"name\":\"Name-3\",\"icon\":\"Icon-3\"},{\"id\":\"4\",\"name\":\"Name-4\",\"icon\":\"Icon-4\"},{\"id\":\"5\",\"name\":\"Name-5\",\"icon\":\"Icon-5\"}]}";

        ApiResult newResult = gson.fromJson(input, ApiResult.class);

        System.out.println(newResult.data); // Array

        input = "{\"status\":\"OK\",\"data\":{\"id\":\"1\",\"name\":\"Name-1\",\"icon\":\"Icon-1\"}}";

        newResult = gson.fromJson(input, ApiResult.class);

        System.out.println(newResult.data); // Object

        input = "{\"status\":\"OK\",\"data\":\"Test\"}";

        newResult = gson.fromJson(input, ApiResult.class);

        System.out.println(newResult.data); // String
Mohamed Shaaban
  • 1,129
  • 6
  • 13
0

I managed to make it work as I wanted, and without using any custom deserializer!

For each endpoint, I wait for the response (btw I'm using Volley), then I first generate the "root" ApiResult object, check if the status is OK, then I proceed instantiating the data field as the requested type.

The POJOs are the same of the question. In ApiResult, "data" is a JsonElement.

// ... called the endpoint that returns a NavItem list
public void onResponse(String response) {
    ApiResult rootResult = gson.fromJson(response.toString(), ApiResult.class);
    if (rootResult.status.equals(STATUS_OK)) {
        Log.d(LOG_TAG, response.toString());
        NavItem.List resData = gson.fromJson(rootResult.data, NavItem.List.class); // <-- !!!!!
        callback.onSuccess(resData);
    }
    else {
        Log.e(LOG_TAG, response.toString());
        callback.onError(-1, null);
    }
}

Obviously the only thing to change for the "Profile" endpoint is the line with !!!!!

TheUnexpected
  • 3,077
  • 6
  • 32
  • 62