Obtener objeto JSON anidado con GSON utilizando retrofit

Estoy consumiendo una API de mi aplicación para Android, y todas las respuestas de JSON son así:

{ 'status': 'OK', 'reason': 'Everything was fine', 'content': { < some data here > } 

El problema es que todos mis POJOs tienen un status , campos de reason , y dentro del campo de content es el POJO real que quiero.

¿Hay alguna manera de crear un convertidor personalizado de Gson para extraer siempre el campo de content , por lo que retrofit devuelve el POJO apropiado?

Escribirías un deserializador personalizado que devuelve el objeto incrustado.

Digamos que su JSON es:

 { "status":"OK", "reason":"some reason", "content" : { "foo": 123, "bar": "some value" } } 

Entonces tendría un Content POJO:

 class Content { public int foo; public String bar; } 

Luego escribe un deserializador:

 class MyDeserializer implements JsonDeserializer<Content> { @Override public Content deserialize(JsonElement je, Type type, JsonDeserializationContext jdc) throws JsonParseException { // Get the "content" element from the parsed JSON JsonElement content = je.getAsJsonObject().get("content"); // Deserialize it. You use a new instance of Gson to avoid infinite recursion // to this deserializer return new Gson().fromJson(content, Content.class); } } 

Ahora si usted construye un Gson con GsonBuilder y registra el deserializador:

 Gson gson = new GsonBuilder() .registerTypeAdapter(Content.class, new MyDeserializer()) .create(); 

Puede deserializar su JSON directamente a su Content :

 Content c = gson.fromJson(myJson, Content.class); 

Editar para añadir comentarios:

Si tiene diferentes tipos de mensajes pero todos tienen el campo "contenido", puede hacer que el Deserializer sea genérico haciendo:

 class MyDeserializer<T> implements JsonDeserializer<T> { @Override public T deserialize(JsonElement je, Type type, JsonDeserializationContext jdc) throws JsonParseException { // Get the "content" element from the parsed JSON JsonElement content = je.getAsJsonObject().get("content"); // Deserialize it. You use a new instance of Gson to avoid infinite recursion // to this deserializer return new Gson().fromJson(content, type); } } 

Sólo tienes que registrar una instancia para cada uno de tus tipos:

 Gson gson = new GsonBuilder() .registerTypeAdapter(Content.class, new MyDeserializer<Content>()) .registerTypeAdapter(DiffContent.class, new MyDeserializer<DiffContent>()) .create(); 

Cuando llama a .fromJson() el tipo se lleva al deserializador, por lo que debería funcionar para todos los tipos.

@ La solución de BrianRoach es la solución correcta. Cabe señalar que en el caso especial en el que tiene objetos personalizados anidados que necesitan un Custom TypeAdapter , debe registrar el TypeAdapter con la nueva instancia de GSON , de lo contrario nunca se llamará al segundo TypeAdapter . Esto se debe a que estamos creando una nueva instancia de Gson dentro de nuestro deserializador personalizado.

Por ejemplo, si tenía el siguiente json:

 { "status": "OK", "reason": "some reason", "content": { "foo": 123, "bar": "some value", "subcontent": { "useless": "field", "data": { "baz": "values" } } } } 

Y quería que este JSON se asignara a los siguientes objetos:

 class MainContent { public int foo; public String bar; public SubContent subcontent; } class SubContent { public String baz; } 

Necesitará registrar el SubContent del TypeAdapter . Para ser más robusto, podría hacer lo siguiente:

 public class MyDeserializer<T> implements JsonDeserializer<T> { private final Class mNestedClazz; private final Object mNestedDeserializer; public MyDeserializer(Class nestedClazz, Object nestedDeserializer) { mNestedClazz = nestedClazz; mNestedDeserializer = nestedDeserializer; } @Override public T deserialize(JsonElement je, Type type, JsonDeserializationContext jdc) throws JsonParseException { // Get the "content" element from the parsed JSON JsonElement content = je.getAsJsonObject().get("content"); // Deserialize it. You use a new instance of Gson to avoid infinite recursion // to this deserializer GsonBuilder builder = new GsonBuilder(); if (mNestedClazz != null && mNestedDeserializer != null) { builder.registerTypeAdapter(mNestedClazz, mNestedDeserializer); } return builder.create().fromJson(content, type); } } 

Y luego crearlo así:

 MyDeserializer<Content> myDeserializer = new MyDeserializer<Content>(SubContent.class, new SubContentDeserializer()); Gson gson = new GsonBuilder().registerTypeAdapter(Content.class, myDeserializer).create(); 

Esto podría ser fácilmente utilizado para el caso de "contenido" anidado, así como pasar simplemente una nueva instancia de MyDeserializer con valores nulos.

Tuvimos el mismo problema hace un par de días. He resuelto esto usando la clase del envoltorio de la respuesta y el transformador de RxJava, que pienso es la solución bastante flexiable:

Envoltura:

 public class ApiResponse<T> { public String status; public String reason; public T content; } 

Excepción personalizada para lanzar, cuando el estado no es correcto:

 public class ApiException extends RuntimeException { private final String reason; public ApiException(String reason) { this.reason = reason; } public String getReason() { return apiError; } } 

Transformador Rx:

 protected <T> Observable.Transformer<ApiResponse<T>, T> applySchedulersAndExtractData() { return observable -> observable .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .map(tApiResponse -> { if (!tApiResponse.status.equals("OK")) throw new ApiException(tApiResponse.reason); else return tApiResponse.content; }); } 

Ejemplo de uso:

 // Call definition: @GET("/api/getMyPojo") Observable<ApiResponse<MyPojo>> getConfig(); // Call invoke: webservice.getMyPojo() .compose(applySchedulersAndExtractData()) .subscribe(this::handleSuccess, this::handleError); private void handleSuccess(MyPojo mypojo) { // handle success } private void handleError(Throwable t) { getView().showSnackbar( ((ApiException) throwable).getReason() ); } 

Mi tema: Retrofit 2 RxJava – Gson – Deserialización "global", tipo de respuesta de cambio

Continuando la idea de Brian, porque casi siempre tenemos muchos recursos REST con su propia raíz, podría ser útil generalizar la deserialización:

  class RestDeserializer<T> implements JsonDeserializer<T> { private Class<T> mClass; private String mKey; public RestDeserializer(Class<T> targetClass, String key) { mClass = targetClass; mKey = key; } @Override public T deserialize(JsonElement je, Type type, JsonDeserializationContext jdc) throws JsonParseException { JsonElement content = je.getAsJsonObject().get(mKey); return new Gson().fromJson(content, mClass); } } 

A continuación, para analizar la carga útil de muestra desde arriba, podemos registrar GSON deserializer:

 Gson gson = new GsonBuilder() .registerTypeAdapter(Content.class, new RestDeserializer<>(Content.class, "content")) .build(); 

Un poco tarde, pero espero que esto ayudará a alguien.

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("content")) { jsonElement = jsonObject.get("content"); } } return delegate.fromJsonTree(jsonElement); } }.nullSafe(); } } 

Y agregarlo en su generador GSON:

 .registerTypeAdapterFactory(new ItemTypeAdapterFactory()); 

o

  yourGsonBuilder.registerTypeAdapterFactory(new ItemTypeAdapterFactory()); 

Esta es la misma solución que @AYarulin, pero supongamos que el nombre de la clase es el nombre de la clave JSON. De esta manera solo necesitas pasar el nombre de la clase.

  class RestDeserializer<T> implements JsonDeserializer<T> { private Class<T> mClass; private String mKey; public RestDeserializer(Class<T> targetClass) { mClass = targetClass; mKey = mClass.getSimpleName(); } @Override public T deserialize(JsonElement je, Type type, JsonDeserializationContext jdc) throws JsonParseException { JsonElement content = je.getAsJsonObject().get(mKey); return new Gson().fromJson(content, mClass); } } 

A continuación, para analizar la carga útil de ejemplo desde arriba, podemos registrar GSON deserializador. Esto es problemático ya que la clave es sensible a mayúsculas y minúsculas, por lo que el caso del nombre de clase debe coincidir con el caso de la clave JSON.

 Gson gson = new GsonBuilder() .registerTypeAdapter(Content.class, new RestDeserializer<>(Content.class)) .build(); 

Esta es una versión de Kotlin basada en las respuestas de Brian Roach y AYarulin.

 class RestDeserializer<T>(targetClass: Class<T>, key: String?) : JsonDeserializer<T> { val targetClass = targetClass val key = key override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): T { val data = json!!.asJsonObject.get(key ?: "") return Gson().fromJson(data, targetClass) } } 

En mi caso, la clave de "contenido" cambiaría para cada respuesta. Ejemplo:

 // Root is hotel { status : "ok", statusCode : 200, hotels : [{ name : "Taj Palace", location : { lat : 12 lng : 77 } }, { name : "Plaza", location : { lat : 12 lng : 77 } }] } //Root is city { status : "ok", statusCode : 200, city : { name : "Vegas", location : { lat : 12 lng : 77 } } 

En tales casos he utilizado una solución similar como se mencionó anteriormente, pero tuvo que ajustarlo. Usted puede ver la esencia aquí . Es un poco demasiado grande para publicarlo aquí en SOF.

La anotación @InnerKey("content") se utiliza y el resto del código es para facilitar su uso con Gson.

Una mejor solución podría ser esto ..

 public class ApiResponse<T> { public T data; public String status; public String reason; } 

Entonces, defina su servicio como este ..

 Observable<ApiResponse<YourClass>> updateDevice(..); 

Según la respuesta de @Brian Roach y @rafakob he hecho esto de la siguiente manera

Respuesta de Json desde el servidor

 { "status": true, "code": 200, "message": "Success", "data": { "fullname": "Rohan", "role": 1 } } 

Clase de controlador de datos común

 public class ApiResponse<T> { @SerializedName("status") public boolean status; @SerializedName("code") public int code; @SerializedName("message") public String reason; @SerializedName("data") public T content; } 

Serializador personalizado

 static class MyDeserializer<T> implements JsonDeserializer<T> { @Override public T deserialize(JsonElement je, Type type, JsonDeserializationContext jdc) throws JsonParseException { JsonElement content = je.getAsJsonObject(); // Deserialize it. You use a new instance of Gson to avoid infinite recursion // to this deserializer return new Gson().fromJson(content, type); } } 

Objeto Gson

 Gson gson = new GsonBuilder() .registerTypeAdapter(ApiResponse.class, new MyDeserializer<ApiResponse>()) .create(); 

Api llamar

  @FormUrlEncoded @POST("/loginUser") Observable<ApiResponse<Profile>> signIn(@Field("email") String username, @Field("password") String password); restService.signIn(username, password) .observeOn(AndroidSchedulers.mainThread()) .subscribeOn(Schedulers.io()) .subscribe(new Observer<ApiResponse<Profile>>() { @Override public void onCompleted() { Log.i("login", "On complete"); } @Override public void onError(Throwable e) { Log.i("login", e.toString()); } @Override public void onNext(ApiResponse<Profile> response) { Profile profile= response.content; Log.i("login", profile.getFullname()); } }); 

Salam. No olvide @SerializedName y @Expose anotaciones para todos los miembros de clase y miembros de clase interna que más deserializado de JSON por GSON.

Mira en https://stackoverflow.com/a/40239512/1676736

  • Retrofit GSON serialize Fecha de json string en java.util.date
  • Uso de MultipartTypedOutput
  • ¿Cómo utilizo el convertidor Gson en RetroFit?
  • ¿Hay un reemplazo para el RequestInterceptor en Retrofit 2?
  • Retrofit no puede acceder a HttpUrl
  • Cómo hacer la solicitud https con certificado SSL en Retrofit
  • Manera correcta de manejar ninguna red con Retrofit y RX-java
  • Lectura de JSON con Retrofit
  • Cómo enviar datos JSON como Cuerpo usando Retrofit android
  • Cómo realizar una prueba de unidad
  • ¿Cómo usar Retrofit y SimpleXML juntos para descargar y analizar un archivo XML de un sitio?
  • FlipAndroid es un fan de Google para Android, Todo sobre Android Phones, Android Wear, Android Dev y Aplicaciones para Android Aplicaciones.