user2393462435 user2393462435 - 18 days ago 14
Android Question

Using Gson and Retrofit 2 to deserialize complex API responses

I'm using

Retrofit 2
and
Gson
and I'm having trouble deserializing responses from my API. Here's my scenario:

I have a model object named
Employee
that has three fields:
id
,
name
,
age
.

I have an API that returns a singular
Employee
object like this:

{
"status": "success",
"code": 200,
"data": {
"id": "123",
"id_to_name": {
"123" : "John Doe"
},
"id_to_age": {
"123" : 30
}
}
}


And a list of
Employee
objects like this:

{
"status": "success",
"code": 200,
"data": [
{
"id": "123",
"id_to_name": {
"123" : "John Doe"
},
"id_to_age": {
"123" : 30
}
},
{
"id": "456",
"id_to_name": {
"456" : "Jane Smith"
},
"id_to_age": {
"456" : 35
}
},
]
}


There are three main things to consider here:


  1. API responses return in a generic wrapper, with the important part inside of the
    data
    field.

  2. The API returns objects in a format that doesn't directly correspond to the fields on the model (for example, the value taken from
    id_to_age
    needs be mapped to the
    age
    field on the model)

  3. The
    data
    field in the API response can be a singular object, or a list of objects.



How do I implement deserialization with
Gson
such that it handles these three cases elegantly?

Ideally, I'd prefer to do this entirely with
TypeAdapter
or
TypeAdapterFactory
instead of paying the performance penalty of
JsonDeserializer
. Ultimately, I want to end up with an instance of
Employee
or
List<Employee>
such that it satisfies this interface:

public interface EmployeeService {

@GET("/v1/employees/{employee_id}")
Observable<Employee> getEmployee(@Path("employee_id") String employeeId);

@GET("/v1/employees")
Observable<List<Employee>> getEmployees();

}


This earlier question I posted discusses my first attempt at this, but it fails to consider a few of the gotchas mentioned above:
Using Retrofit and RxJava, how do I deserialize JSON when it doesn't map directly to a model object?

Answer

EDIT: Relevant update: creating a custom converter factory DOES work--the key to avoiding an infinite loop through ApiResponseConverterFactory's is to call Retrofit's nextResponseBodyConverter which allows you to specify a factory to skip over. The key is this would be a Converter.Factory to register with Retrofit, not a TypeAdapterFactory for Gson. This would actually be preferable since it prevents double-deserialization of the ResponseBody (no need to deserialize the body then repackage it again as another response).

See the gist here for an implementation example.

ORIGINAL ANSWER:

The ApiResponseAdapterFactory approach doesn't work unless you are willing to wrap all your service interfaces with ApiResponse<T>. However, there is another option: OkHttp interceptors.

Here's our strategy:

  • For the particular retrofit configuration, you will register an application interceptor that intercepts the Response
  • Response#body() will be deserialized as an ApiResponse and we return a new Response where the ResponseBody is just the content we want.

So ApiResponse looks like:

public class ApiResponse {
  String status;
  int code;
  JsonObject data;
}

ApiResponseInterceptor:

public class ApiResponseInterceptor implements Interceptor {
  public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
  public static final Gson GSON = new Gson();

  @Override
  public Response intercept(Chain chain) throws IOException {
    Request request = chain.request();
    Response response = chain.proceed(request);
    final ResponseBody body = response.body();
    ApiResponse apiResponse = GSON.fromJson(body.string(), ApiResponse.class);
    body.close();

    // TODO any logic regarding ApiResponse#status or #code you need to do 

    final Response.Builder newResponse = response.newBuilder()
        .body(ResponseBody.create(JSON, apiResponse.data.toString()));
    return newResponse.build();
  }
}

Configure your OkHttp and Retrofit:

OkHttpClient client = new OkHttpClient.Builder()
        .addInterceptor(new ApiResponseInterceptor())
        .build();
Retrofit retrofit = new Retrofit.Builder()
        .client(client)
        .build();

And Employee and EmployeeResponse should follow the adapter factory construct I wrote in the previous question. Now all of the ApiResponse fields should be consumed by the interceptor and every Retrofit call you make should only return the JSON content you are interested in.