Using Gson and Retrofit 2 to deserialize complex API responses
I would suggest using a JsonDeserializer
because there is not so many levels of nesting in the response, so it won't be a big performance hit.
Classes would look something like this:
Service interface needs to be adjusted for the generic response:
interface EmployeeService {
@GET("/v1/employees/{employee_id}")
Observable<DataResponse<Employee>> getEmployee(@Path("employee_id") String employeeId);
@GET("/v1/employees")
Observable<DataResponse<List<Employee>>> getEmployees();
}
This is a generic data response:
class DataResponse<T> {
@SerializedName("data") private T data;
public T getData() {
return data;
}
}
Employee model:
class Employee {
final String id;
final String name;
final int age;
Employee(String id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
}
Employee deserializer:
class EmployeeDeserializer implements JsonDeserializer<Employee> {
@Override
public Employee deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
JsonObject employeeObject = json.getAsJsonObject();
String id = employeeObject.get("id").getAsString();
String name = employeeObject.getAsJsonObject("id_to_name").entrySet().iterator().next().getValue().getAsString();
int age = employeeObject.getAsJsonObject("id_to_age").entrySet().iterator().next().getValue().getAsInt();
return new Employee(id, name, age);
}
}
The problem with the response is that name
and age
are contained inside of an JSON object whitch translates to a Map in Java so it requires a bit more work to parse it.
Just create following TypeAdapterFactory.
public class ItemTypeAdapterFactory implements TypeAdapterFactory {
public <T> TypeAdapter<T> create(Gson gson, final TypeToken<T> type) {
final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
final TypeAdapter<JsonElement> elementAdapter = gson.getAdapter(JsonElement.class);
return new TypeAdapter<T>() {
public void write(JsonWriter out, T value) throws IOException {
delegate.write(out, value);
}
public T read(JsonReader in) throws IOException {
JsonElement jsonElement = elementAdapter.read(in);
if (jsonElement.isJsonObject()) {
JsonObject jsonObject = jsonElement.getAsJsonObject();
if (jsonObject.has("data")) {
jsonElement = jsonObject.get("data");
}
}
return delegate.fromJsonTree(jsonElement);
}
}.nullSafe();
}
}
and add it into your GSON builder :
.registerTypeAdapterFactory(new ItemTypeAdapterFactory());
or
yourGsonBuilder.registerTypeAdapterFactory(new ItemTypeAdapterFactory());
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 anApiResponse
and we return a newResponse
where theResponseBody
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.