Obtenga un objeto JSON anidado con GSON usando la modificación

111

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

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

El problema es que todos mis POJO status, reasoncampos, y dentro del contentcampo es el POJO real Quiero.

¿Hay alguna forma de crear un convertidor personalizado de Gson para extraer siempre el contentcampo, por lo que la actualización devuelve el POJO apropiado?

mikelar
fuente
Leí el documento pero no veo cómo hacerlo ... :( No me doy cuenta de cómo programar el código para resolver mi problema
mikelar
Tengo curiosidad por saber por qué no formatearía su clase POJO para manejar esos resultados de estado.
jj.

Respuestas:

168

Escribiría un deserializador personalizado que devuelva el objeto incrustado.

Digamos que su JSON es:

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

Entonces tendrías un ContentPOJO:

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 construye un Gsoncon GsonBuildery 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 agregar desde los comentarios:

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

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

    }
}

Solo 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 llamas, .fromJson()el tipo se lleva 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();
Brian Roach
fuente
1
¡Wow eso es genial! ¡Gracias! : D ¿Hay alguna forma de generalizar la solución para no tener que crear un JsonDeserializer por cada tipo de respuesta?
mikelar
1
¡Esto es increíble! Una cosa para cambiar: Gson gson = new GsonBuilder (). Create (); En lugar de Gson gson = new GsonBuilder (). Build (); Hay dos casos de esto.
Nelson Osacky
7
@feresr puede llamar setConverter(new GsonConverter(gson))en la RestAdapter.Builderclase de Retrofit
akhy
2
@BrianRoach gracias, buena respuesta .. ¿Debería registrarme Person.classy List<Person>.class/ Person[].classcon Deserializer separado?
akhy
2
¿Alguna posibilidad de obtener el "estado" y la "razón" también? Por ejemplo, si todas las solicitudes las devuelven, ¿podemos tenerlas en una superclase y usar subclases que son los POJO reales del "contenido"?
Nima G
14

La solución de @ BrianRoach es la solución correcta. Vale la pena señalar que en el caso especial en el que ha anidado objetos personalizados que necesitan un personalizado TypeAdapter, debe registrar el TypeAdaptercon la nueva instancia de GSON ; de lo contrario TypeAdapter, nunca se llamará al segundo . Esto se debe a que estamos creando una nueva Gsoninstancia 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 deseaba 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;
}

Usted tendría que registrar los SubContent's TypeAdapter. Para ser más robusto, puede 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 créalo así:

MyDeserializer<Content> myDeserializer = new MyDeserializer<Content>(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" anidado simplemente pasando una nueva instancia de MyDeserializercon valores nulos.

KMarlow
fuente
1
¿De qué paquete es "Tipo"? Hay un millón de paquetes que contienen la clase "Tipo". Gracias.
Kyle Bridenstine
2
@ Mr.Tea Serájava.lang.reflect.Type
aidan
1
¿Dónde está la clase SubContentDeserializer? @KMarlow
LogronJ
10

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

Simplemente cree el siguiente 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 agréguelo a su constructor GSON:

.registerTypeAdapterFactory(new ItemTypeAdapterFactory());

o

 yourGsonBuilder.registerTypeAdapterFactory(new ItemTypeAdapterFactory());
Matin Petrulak
fuente
Esto es exactamente lo que miro. Porque tengo muchos tipos envueltos con el nodo "datos" y no puedo agregar TypeAdapter a cada uno de ellos. ¡Gracias!
Sergey Irisov
@SergeyIrisov de nada. Puedes votar esta respuesta para que sea más alta :)
Matin Petrulak
¿Cómo pasar múltiples jsonElement?. como tengo content, content1, etc.
Sathish Kumar J
Creo que sus desarrolladores de back-end deberían cambiar la estructura y no pasar contenido, content1 ... ¿Cuál es la ventaja de este enfoque?
Matin Petrulak
¡Gracias! Esta es la respuesta perfecta. @Marin Petrulak: La ventaja es que este formulario está preparado para cambios en el futuro. "contenido" es el contenido de la respuesta. En el futuro pueden aparecer nuevos campos como "versión", "lastUpdated", "sessionToken" y así sucesivamente. Si no envolvió el contenido de su respuesta de antemano, se encontrará con un montón de soluciones en su código para adaptarse a la nueva estructura.
muetzenflo
7

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

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

Uso de ejemplo:

// 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", cambio de tipo de respuesta

rafakob
fuente
¿Qué aspecto tiene MyPojo?
IgorGanapolsky
1
@IgorGanapolsky MyPojo puede lucir como quieras. Debe coincidir con los datos de contenido recuperados de un servidor. La estructura de esta clase debe ajustarse a su convertidor de serialización (Gson, Jackson, etc.).
rafakob
@rafakob, ¿puedes ayudarme también con mi problema? Me resulta difícil intentar obtener un campo en mi json anidado de la manera más simple posible. aquí está mi pregunta: stackoverflow.com/questions/56501897/…
6

Continuando con la idea de Brian, dado que casi siempre tenemos muchos recursos REST cada uno 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);

    }
}

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

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

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

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

Luego, defina su servicio así ...

Observable<ApiResponse<YourClass>> updateDevice(..);
Federico J Farina
fuente
3

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

Respuesta json del servidor

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

Clase de manejador 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();

Llamada de API

 @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());
                    }
                });
Rohan Pawar
fuente
2

Esta es la misma solución que @AYarulin pero suponga que el nombre de la clase es el nombre de la clave JSON. De esta manera solo necesita 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);

    }
}

Luego, para analizar la carga útil de muestra de 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 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();
Barry MSIH
fuente
2

Aquí hay 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)
    }
}
RamwiseMatt
fuente
1

En mi caso, la clave "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 enumerada anteriormente, pero tuve que modificarla. Puedes ver la esencia aquí . Es demasiado grande para publicarlo aquí en SOF.

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

Varun Achar
fuente
¿Puedes ayudarme también con mi pregunta? Tómese un tiempo para obtener un campo de un json anidado de la manera más simple posible: stackoverflow.com/questions/56501897/…
0

No se olvide @SerializedNamey @Exposeanotaciones para todos los miembros de la clase y los miembros de la clase interna que la mayoría de deserializado JSON por GSON.

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

Sayed Abolfazl Fatemi
fuente
0

Otra solución simple:

JsonObject parsed = (JsonObject) new JsonParser().parse(jsonString);
Content content = gson.fromJson(parsed.get("content"), Content.class);
Vadzim
fuente