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.
- Enviar archivos multiparte con retrofit: 2.0.0-beta1
- Hilo predeterminado de actualización
- Retrofit 2 cómo obtener la respuesta de objetos anidados json (android)
- RxJava onCompleted y onTerminate en el hilo principal
- Runtime Execption java.lang.NoClassDefFoundError: retrofit2.Platform en Android
¿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?
- Desbordamiento de pila al utilizar Retrofit rxjava concatWith
- Cómo poner en cola las llamadas de la API cuando no está conectado mientras usa Retrofit
- Prueba de unidad de Android con Retrofit y Mockito
- Retrofit y GET utilizando parámetros
- Operadores de manipulación de errores de Retrofit 2 y RxJava
- Primavera Android Vs Retrofit
- Excepción fatal lanzada en el subprograma Scheduler.Worker con Retrofit 2 y Rx
- ¿Cómo llamo a una intención en Retrofit Callback?
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.
- Prueba Android AsyncTask con Android Test Framework
- "Parse Error: hay un problema al analizar el paquete" mientras instala la aplicación de Android