Obtiene el objeto JSON nested con GSON usando la modificación

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

{ 'status': 'OK', 'reason': 'Everything was fine', 'content': {  } 

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

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

Debería escribir un deserializador personalizado que devuelva el objeto incrustado.

Digamos que tu JSON es:

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

Entonces tendrías un POJO de Content :

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

Entonces escribes un deserializador:

 class MyDeserializer implements JsonDeserializer { @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 construyes un Gson con GsonBuilder y registras 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 agregar de los comentarios:

Si tiene diferentes tipos de mensajes pero todos tienen el campo “contenido”, puede hacer que el deserializador sea genérico al hacer:

 class MyDeserializer implements JsonDeserializer { @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); } } 

Solo tiene que registrar una instancia para cada uno de sus tipos:

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

Cuando llamas a .fromJson() el tipo se transporta al deserializador, por lo que debería funcionar para todos tus tipos.

Y finalmente al crear una instancia de Retrofit:

 Retrofit retrofit = new Retrofit.Builder() .baseUrl(url) .addConverterFactory(GsonConverterFactory.create(gson)) .build(); 

La solución de @ BrianRoach es la solución correcta. Vale la pena señalar que en el caso especial donde haya nested objetos personalizados que necesitan un TypeAdapter personalizado, debe registrar el TypeAdapter con la nueva instancia de GSON ; de lo contrario, nunca se TypeAdapter el segundo TypeAdapter . Esto se debe a que estamos creando una nueva instancia de Gson dentro de nuestro deserializador personalizado.

Por ejemplo, si tuviera 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; } 

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

 public class MyDeserializer implements JsonDeserializer { 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 créalo así:

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

Esto también podría usarse fácilmente para el caso de “contenido” nested simplemente pasando una nueva instancia de MyDeserializer con valores nulos.

Un poco tarde, pero espero que esto ayude a alguien.

Simplemente crea lo siguiente TypeAdapterFactory.

  public class ItemTypeAdapterFactory implements TypeAdapterFactory { public  TypeAdapter create(Gson gson, final TypeToken type) { final TypeAdapter delegate = gson.getDelegateAdapter(this, type); final TypeAdapter elementAdapter = gson.getAdapter(JsonElement.class); return new TypeAdapter() { 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 agrégalo a tu generador de GSON:

 .registerTypeAdapterFactory(new ItemTypeAdapterFactory()); 

o

  yourGsonBuilder.registerTypeAdapterFactory(new ItemTypeAdapterFactory()); 

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

  class RestDeserializer implements JsonDeserializer { private Class mClass; private String mKey; public RestDeserializer(Class 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); } } 

Luego, para analizar la carga útil de la muestra desde arriba, podemos registrar el deserializador GSON:

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

Tuve el mismo problema hace un par de días. Lo he resuelto usando la clase de envoltorio de respuesta y el transformador RxJava, que creo que es una solución bastante flexible:

Envoltura:

 public class ApiResponse { 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 de Rx:

 protected  Observable.Transformer, 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> 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 al cambio

Esta es la misma solución que @AYarulin pero asume que el nombre de la clase es el nombre de la clave JSON. De esta forma solo necesita pasar el nombre de la clase.

  class RestDeserializer implements JsonDeserializer { private Class mClass; private String mKey; public RestDeserializer(Class 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); } } 

Luego, para analizar la carga de muestra desde arriba, podemos registrar el deserializador GSON. Esto es problemático ya que la clave distingue entre mayúsculas y minúsculas, por lo que el caso del nombre de la clase debe coincidir con el caso de la clave JSON.

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

Una mejor solución podría ser esta …

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

Luego, defina su servicio así …

 Observable> updateDevice(..); 

Aquí hay una versión de Kotlin basada en las respuestas de Brian Roach y AYarulin.

 class RestDeserializer(targetClass: Class, key: String?) : JsonDeserializer { 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) } } 

Según la respuesta de @Brian Roach y @rafakob, hice esto de la siguiente manera

Json respuesta del servidor

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

Clase de manejador de datos comunes

 public class ApiResponse { @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 implements JsonDeserializer { @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()) .create(); 

Llamada de Api

  @FormUrlEncoded @POST("/loginUser") Observable> signIn(@Field("email") String username, @Field("password") String password); restService.signIn(username, password) .observeOn(AndroidSchedulers.mainThread()) .subscribeOn(Schedulers.io()) .subscribe(new Observer>() { @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 response) { Profile profile= response.content; Log.i("login", profile.getFullname()); } }); 

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, utilicé una solución similar a la lista arriba, pero tuve que ajustarla. Puedes ver la esencia aquí . Es un poco demasiado grande para publicarlo aquí en SOF.

Se @InnerKey("content") anotación @InnerKey("content") y el rest del código es para facilitar su uso con Gson.

Salam. No se olvide de @SerializedName y @Expose anotaciones para todos los miembros de la Clase y los miembros de la Clase interna que GSO haya deserializado de JSON.

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