Ottieni oggetto JSON nidificato con GSON utilizzando il retrofit


111

Sto consumando un'API dalla mia app Android e tutte le risposte JSON sono come questa:

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

Il problema è che tutti i miei POJO hanno un status, reasoncampi, e dentro il contentcampo è il vero POJO che voglio.

C'è un modo per creare un convertitore personalizzato di Gson per estrarre sempre il contentcampo, quindi il retrofit restituisce il POJO appropriato?



Ho letto il documento ma non vedo come farlo ... :( Non mi rendo conto di come programmare il codice per risolvere il mio problema
mikelar

Sono curioso di sapere perché non formatteresti semplicemente la tua classe POJO per gestire quei risultati di stato.
jj.

Risposte:


168

Scriveresti un deserializzatore personalizzato che restituisca l'oggetto incorporato.

Supponiamo che il tuo JSON sia:

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

Avresti quindi un ContentPOJO:

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

Quindi scrivi un deserializzatore:

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

    }
}

Ora se costruisci un Gsoncon GsonBuildere registri il deserializzatore:

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

Puoi deserializzare il tuo JSON direttamente nel tuo Content:

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

Modifica per aggiungere dai commenti:

Se hai diversi tipi di messaggi ma tutti hanno il campo "contenuto", puoi rendere generico il Deserializzatore facendo:

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

    }
}

Devi solo registrare un'istanza per ciascuno dei tuoi tipi:

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

Quando chiami .fromJson()il tipo viene trasferito nel deserializzatore, quindi dovrebbe funzionare per tutti i tuoi tipi.

Infine, quando si crea un'istanza di retrofit:

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

1
Wow è fantastico! Grazie! : D C'è un modo per generalizzare la soluzione in modo da non dover creare un JsonDeserializer per ogni tipo di risposta?
mikelar

1
È fantastico! Una cosa da cambiare: Gson gson = new GsonBuilder (). Create (); Invece di Gson gson = new GsonBuilder (). Build (); Ci sono due casi di ciò.
Nelson Osacky

7
@feresr puoi chiamare setConverter(new GsonConverter(gson))nella RestAdapter.Builderclasse di Retrofit
akhy

2
@BrianRoach grazie, bella risposta .. devo registrarmi Person.classe List<Person>.class/ Person[].classcon Deserializer separato?
akhy

2
Qualche possibilità di ottenere anche lo "stato" e il "motivo"? Ad esempio, se tutte le richieste le restituiscono, possiamo averle in una super classe e utilizzare sottoclassi che sono i POJO effettivi da "contenuto"?
Nima G,

14

La soluzione di @ BrianRoach è la soluzione corretta. Vale la pena notare che nel caso speciale in cui si hanno oggetti personalizzati annidati che necessitano entrambi di un custom TypeAdapter, è necessario registrare il TypeAdaptercon la nuova istanza di GSON , altrimenti la seconda TypeAdapternon verrà mai chiamata. Questo perché stiamo creando una nuova Gsonistanza all'interno del nostro deserializzatore personalizzato.

Ad esempio, se avessi il seguente json:

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

E volevi che questo JSON fosse mappato ai seguenti oggetti:

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

class SubContent
{
    public String baz;
}

Dovresti registrare il file SubContent's TypeAdapter. Per essere più robusto, potresti fare quanto segue:

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

    }
}

e poi crearlo in questo modo:

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

Questo potrebbe essere facilmente utilizzato anche per il caso "contenuto" annidato semplicemente passando una nuova istanza di MyDeserializercon valori nulli.


1
Da quale pacchetto proviene il "Tipo"? Ci sono un milione di pacchetti contenenti la classe "Tipo". Grazie.
Kyle Bridenstine

2
@ Mr.Tea Saràjava.lang.reflect.Type
aidan

1
Dov'è la classe SubContentDeserializer? @KMarlow
LogronJ

10

Un po 'tardi, ma si spera che questo possa aiutare qualcuno.

Basta creare il seguente 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();
    }
}

e aggiungilo al tuo builder GSON:

.registerTypeAdapterFactory(new ItemTypeAdapterFactory());

o

 yourGsonBuilder.registerTypeAdapterFactory(new ItemTypeAdapterFactory());

Questo è esattamente quello che guardo. Perché ho molti tipi inseriti nel nodo "dati" e non posso aggiungere TypeAdapter a ciascuno di essi. Grazie!
Sergey Irisov

@SergeyIrisov sei il benvenuto. Puoi votare questa risposta in modo che
aumenti

Come passare più jsonElement?. come ho content, content1e così via
Sathish Kumar J

Penso che i tuoi sviluppatori back-end dovrebbero cambiare la struttura e non passare contenuto, contenuto1 ... Qual è il vantaggio di questo approccio?
Matin Petrulak

Grazie! Questa è la risposta perfetta. @Marin Petrulak: Il vantaggio è che questo modulo è a prova di futuro per i cambiamenti. "contenuto" è il contenuto della risposta. In futuro potrebbero arrivare nuovi campi come "version", "lastUpdated", "sessionToken" e così via. Se non hai avvolto il contenuto della risposta in anticipo, ti imbatterai in una serie di soluzioni alternative nel tuo codice per adattarlo alla nuova struttura.
muetzenflo

7

Ho avuto lo stesso problema un paio di giorni fa. Ho risolto questo problema utilizzando la classe wrapper di risposta e il trasformatore RxJava, che penso sia una soluzione abbastanza flessibile:

Wrapper:

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

Eccezione personalizzata da lanciare, quando lo stato non è OK:

public class ApiException extends RuntimeException {
    private final String reason;

    public ApiException(String reason) {
        this.reason = reason;
    }

    public String getReason() {
        return apiError;
    }
}

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

Utilizzo di esempio:

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

Il mio argomento: Retrofit 2 RxJava - Gson - Deserializzazione "globale", modifica del tipo di risposta


Che aspetto ha MyPojo?
IgorGanapolsky

1
@IgorGanapolsky MyPojo può apparire come vuoi. Dovrebbe corrispondere ai dati del contenuto recuperati da un server. La struttura di questa classe dovrebbe essere adattata al convertitore di serializzazione (Gson, Jackson ecc.).
rafakob

@rafakob puoi aiutarmi anche con il mio problema? Ho difficoltà a cercare di ottenere un campo nel mio json annidato nel modo più semplice possibile. ecco la mia domanda: stackoverflow.com/questions/56501897/...

6

Continuando l'idea di Brian, poiché abbiamo quasi sempre molte risorse REST ciascuna con la propria radice, potrebbe essere utile generalizzare la deserializzazione:

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

    }
}

Quindi per analizzare il payload di esempio dall'alto, possiamo registrare il deserializzatore GSON:

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

3

Una soluzione migliore potrebbe essere questa ..

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

Quindi, definisci il tuo servizio in questo modo.

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

3

Come da risposta di @Brian Roach e @rafakob, l'ho fatto nel modo seguente

Risposta Json dal server

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

Classe del gestore dati comune

public class ApiResponse<T> {
    @SerializedName("status")
    public boolean status;

    @SerializedName("code")
    public int code;

    @SerializedName("message")
    public String reason;

    @SerializedName("data")
    public T content;
}

Serializzatore personalizzato

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

      }
}

Oggetto Gson

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

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

2

Questa è la stessa soluzione di @AYarulin ma presuppone che il nome della classe sia il nome della chiave JSON. In questo modo devi solo passare il nome della classe.

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

    }
}

Quindi, per analizzare il payload di esempio dall'alto, possiamo registrare il deserializzatore GSON. Ciò è problematico in quanto la chiave distingue tra maiuscole e minuscole, quindi il nome della classe deve corrispondere a quello della chiave JSON.

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

2

Ecco una versione di Kotlin basata sulle risposte di Brian Roach e 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)
    }
}

1

Nel mio caso, la chiave "contenuto" cambierebbe per ogni risposta. Esempio:

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

In questi casi ho usato una soluzione simile a quella sopra elencata, ma ho dovuto modificarla. Puoi vedere il succo qui . È un po 'troppo grande per pubblicarlo qui su SOF.

L'annotazione @InnerKey("content")viene utilizzata e il resto del codice serve a facilitarne l'utilizzo con Gson.


Puoi aiutare anche con la mia domanda. Hanno un tempo HRD ottenere un campo da un JSON nidificato nel modo più semplice possibile: stackoverflow.com/questions/56501897/...


0

Un'altra semplice soluzione:

JsonObject parsed = (JsonObject) new JsonParser().parse(jsonString);
Content content = gson.fromJson(parsed.get("content"), Content.class);
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.