Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Adapting Retrofit responses using GSON

I would like to agnostically retrieve the child element of a known JSON object with every successful response I receive from a particular API.

Each server response returns the following JSON format (condensed for simplicity):

{
    "status": "success",
    "error_title": "",
    "error_message": "",
    "data": {
        "messages": [
            { "message_id": "123",
              "content": "This is a message" },
            { "message_id": "124",
              "content": "This is another message" }
        ]
    }
}

Error responses contain the same, general format, with the "data" object being empty and the error-related JSON objects containing useful values. In the case of an error, I would like to extract the error-related JSON objects.

With the above response, I have a MessageResponse class that contains status, errorTitle, and errorMessage String properties as well as a MessageData object. The MessageData object then contains a list of messages - List<Message> messages. My GET method for getting messages in this case is as follows (condensed for simplicity):

@GET("/chat/conversation")
void getMessages(Callback<MessageResponse> callback);

This design requires three classes for each response type if I were to stick to the default POJO mapping that GSON's serializer provides out-of-box. My end goal is to cut down on the amount of classes necessary by reading only what I need from a successful server response and ignoring the rest. I would like all my success, callback data types on this API to be as close to the "data" content as possible.

In other words, I would like to agnostically return the child element of "data". In the case above, it is an array called "messages", but in some other response it could be a "user" object, for example. I know this can be done by registering separate TypeAdapters for each response type, but I would like to achieve my end goal by using a single, generic solution.

UPDATE: Implementation of David's suggestion from below

public class BaseResponse<T> {
     @SerializedName("status") public String status;
     @SerializedName("error_title") public String errorMessageTitle;
     @SerializedName("error_message") public String errorMessage;
     @SerializedName("data") public T data;
}

public class MessagesResponse extends BaseResponseData<List<Message>> {
     @SerializedName("messages") List<Message> messages;
}

@GET("/chat/conversation")
void getMessages(Callback<BaseResponse<MessageResponse>> callback);

Unfortunately this is not getting serialized properly. If only I could somehow inform GSON of a variably-named JSON object child from the "data" parent and deserialize that child into a model class that is referred to by a generic data type. Essentially, dataJsonObject.getChild().

like image 408
Ryan Avatar asked May 08 '15 02:05

Ryan


1 Answers

After a few hours of unsuccessfully feeding generic, base response classes to GSON, I ended up passing on that route and settling on a solution that I implemented a few days ago (minus the status check conditionals).

GSON provides the ability to add a TypeAdapter to all responses by defining deserialization logic in a generic TypeAdapterFactory. This entity is not as clean and ignorant as I was hoping for it to be, but it does the job in achieving a reduction in the number of necessary response model classes while also maintaining a single adapter.

private static class ResponseTypeAdapterFactory implements TypeAdapterFactory {

    private static final String STATUS = "status";
    private static final String SUCCESS = "success";
    private static final String DATA = "data";

    @Override
    public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
        final TypeAdapter<T> delegateAdapter = gson.getDelegateAdapter(this, type);
        final TypeAdapter<JsonElement> jsonElementAdapter = gson.getAdapter(JsonElement.class);

        return new TypeAdapter<T>() {
            @Override
            public void write(JsonWriter out, T value) throws IOException {
                delegateAdapter.write(out, value);
            }

            @Override
            public T read(JsonReader in) throws IOException {
                // Ignore extraneous data and read in only the response data when the response is a success
                JsonElement jsonElement = jsonElementAdapter.read(in);
                if (jsonElement.isJsonObject()) {
                    JsonObject jsonObject = jsonElement.getAsJsonObject();
                    if (jsonObject.has(STATUS)) {
                        if (jsonObject.get(STATUS).getAsString().equals(SUCCESS)) {
                            if (jsonObject.has(DATA) && jsonObject.get(DATA).isJsonObject()) {
                                jsonElement = jsonObject.get(DATA);
                            }
                        }
                    }
                }
                return delegateAdapter.fromJsonTree(jsonElement);
            }
        }.nullSafe();
    }
}

In a nutshell, I'm telling GSON to grab the "data" JSON object if the response was successful. Otherwise, return the entire response body so that my custom, Retrofit error handler can make use of the "error_title" and "error_message" fields returned from the server.

A huge shoutout to @david.mihola for the great suggestions and eventually directing my attention back to the TypeAdapterFactory solution.

like image 184
Ryan Avatar answered Sep 23 '22 00:09

Ryan