Uso de Gson y Retrofit 2 para deserializar las complejas respuestas de la API

Estoy usando Retrofit 2 y Gson y estoy teniendo problemas para deserializar las respuestas de mi API. Aquí está mi escenario:

Tengo un objeto modelo llamado Employee que tiene tres campos: id , name , age .

Tengo una API que devuelve un objeto Employee singular como este:

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

Y una lista de objetos de Employee como este:

 { "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 } }, ] } 

Hay tres cosas principales a considerar aquí:

  1. Las respuestas de API vuelven en un contenedor genérico, con la parte importante dentro del campo de data .
  2. La API devuelve objetos en un formato que no corresponde directamente a los campos del modelo (por ejemplo, el valor tomado de id_to_age necesita ser mapeado al campo de age del modelo)
  3. El campo de data en la respuesta de la API puede ser un objeto singular o una lista de objetos.

¿Cómo implemento la deserialización con Gson manera que maneje estos tres casos elegantemente?

Idealmente, prefiero hacerlo completamente con TypeAdapter o TypeAdapterFactory lugar de pagar la pena de rendimiento de JsonDeserializer . En última instancia, quiero terminar con una instancia de Employee o List<Employee> tal manera que satisfaga esta interfaz:

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

Esta pregunta anterior que publiqué discute mi primer intento en esto, pero no considera algunas de las gotchas mencionadas anteriormente: ¿ Utilizando Retrofit y RxJava, cómo deserializar JSON cuando no se mapea directamente a un objeto modelo?

EDIT: actualización relevante: la creación de una fábrica de conversor personalizado funciona – la clave para evitar un bucle infinito a través de ApiResponseConverterFactory 's es llamar nextResponseBodyConverter de nextResponseBodyConverter que le permite especificar una fábrica para saltar. La clave es que esto sería un Converter.Factory para registrarse con Retrofit, no un TypeAdapterFactory para Gson. Esto sería realmente preferible ya que evita la deserialización doble del ResponseBody (no es necesario deserializar el cuerpo y volver a empaquetarlo de nuevo como otra respuesta).

Vea aquí la idea de un ejemplo de implementación.

RESPUESTA ORIGINAL:

El método ApiResponseAdapterFactory no funciona a menos que esté dispuesto a ajustar todas sus interfaces de servicio con ApiResponse<T> . Sin embargo, hay otra opción: OkHttp interceptores.

Aquí está nuestra estrategia:

  • Para la configuración de adaptación particular, registrará un interceptor de aplicación que intercepta la Response
  • Response#body() será deserializada como ApiResponse y devolveremos una nueva Response donde el ResponseBody es sólo el contenido que queremos.

Así que ApiResponse parece a:

 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 su OkHttp y Retrofit:

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

Y Employee y EmployeeResponse deben seguir la construcción de adaptador de fábrica que escribí en la pregunta anterior . Ahora todos los campos ApiResponse deben ser consumidos por el interceptor y cada llamada de Retrofit que realice debe devolver el contenido de JSON que le interesa.

Yo sugeriría usar un JsonDeserializer porque no hay tantos niveles de anidamiento en la respuesta, por lo que no será un gran éxito de rendimiento.

Las clases se verían así:

La interfaz de servicio necesita ser ajustada para la respuesta genérica:

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

Esta es una respuesta genérica de datos:

 class DataResponse<T> { @SerializedName("data") private T data; public T getData() { return data; } } 

Modelo empleado:

 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; } } 

Deserializador de empleados:

 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); } } 

El problema con la respuesta es que el name y la age están contenidos dentro de un objeto JSON whitch se traduce en un mapa en Java por lo que requiere un poco más de trabajo para analizarlo.

Simplemente cree después de 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(); } 

}

Y agregarlo en su generador GSON:

 .registerTypeAdapterFactory(new ItemTypeAdapterFactory()); 

o

  yourGsonBuilder.registerTypeAdapterFactory(new ItemTypeAdapterFactory()); 
  • Retrofit 2 tutorial ejemplo, pero GsonConverterFactory error de visualización "No se puede resolver el símbolo"
  • RecyclerView no se actualiza después de NotifyDataSetChanged (), el contenido aparece después de encender y apagar la pantalla
  • Cómo crear un objeto retrofit.Response durante las pruebas de unidad con Retrofit 2
  • Solicitudes HTTP periódicas con RxJava y Retrofit
  • Cómo implementar un WebSocket con Retrofit + RX
  • Manera correcta de manejar ninguna red con Retrofit y RX-java
  • Modificación retroactiva de caracteres especiales
  • Solicitud por lotes utilizando Retrofit
  • Uso de Retrofit para acceder a una API con key / id
  • Retrofit POST con un objeto json que contiene parámetros
  • Error: tipos incompatibles: GsonConverterFactory no se puede convertir en Factory
  • FlipAndroid es un fan de Google para Android, Todo sobre Android Phones, Android Wear, Android Dev y Aplicaciones para Android Aplicaciones.