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 TypeAdapter
s 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()
.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With