Holen Sie sich verschachteltes JSON-Objekt mit GSON durch Nachrüstung

111

Ich verwende eine API aus meiner Android-App und alle JSON-Antworten lauten wie folgt:

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

Das Problem ist, dass alle meine POJOs ein status, reasonFelder haben und innerhalb des contentFeldes das echte POJO ist, das ich möchte.

Gibt es eine Möglichkeit, einen benutzerdefinierten Konverter von Gson zu erstellen, um immer das contentFeld zu extrahieren , sodass die Nachrüstung das entsprechende POJO zurückgibt?

mikelar
quelle
Ich habe das Dokument gelesen, aber ich sehe nicht, wie es geht ... :( Ich weiß nicht, wie ich den Code programmieren soll, um mein Problem zu lösen
mikelar
Ich bin gespannt, warum Sie Ihre POJO-Klasse nicht einfach so formatieren würden, dass diese Statusergebnisse verarbeitet werden.
jj.

Antworten:

168

Sie würden einen benutzerdefinierten Deserializer schreiben, der das eingebettete Objekt zurückgibt.

Angenommen, Ihr JSON lautet:

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

Sie hätten dann ein ContentPOJO:

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

Dann schreiben Sie einen Deserializer:

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

    }
}

Wenn Sie nun einen Gsonmit GsonBuildererstellen und den Deserializer registrieren:

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

Sie können Ihren JSON direkt auf Folgendes deserialisieren Content:

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

Bearbeiten, um aus Kommentaren hinzuzufügen:

Wenn Sie verschiedene Arten von Nachrichten haben, aber alle das Feld "Inhalt" haben, können Sie den Deserializer generisch machen, indem Sie Folgendes tun:

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

    }
}

Sie müssen nur eine Instanz für jeden Ihrer Typen registrieren:

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

Wenn Sie .fromJson()den Typ aufrufen, wird er in den Deserializer übertragen, sodass er für alle Ihre Typen funktionieren sollte.

Und schließlich beim Erstellen einer Retrofit-Instanz:

Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(url)
                .addConverterFactory(GsonConverterFactory.create(gson))
                .build();
Brian Roach
quelle
1
wow! Das ist toll! Vielen Dank! : D Gibt es eine Möglichkeit, die Lösung zu verallgemeinern, damit ich nicht für jeden Antworttyp einen JsonDeserializer erstellen muss?
Mikelar
1
Das ist großartig! Eine Sache, die geändert werden muss: Gson gson = new GsonBuilder (). Create (); Anstelle von Gson gson = new GsonBuilder (). Build (); Dafür gibt es zwei Beispiele.
Nelson Osacky
7
@feresr können Sie setConverter(new GsonConverter(gson))in Retrofits RestAdapter.BuilderKlasse
anrufen
2
@BrianRoach danke, nette Antwort .. soll ich mich registrieren Person.classund List<Person>.class/ oder Person[].classmit getrenntem Deserializer?
Akhy
2
Gibt es eine Möglichkeit, auch den "Status" und den "Grund" zu erhalten? Wenn zum Beispiel alle Anfragen sie zurückgeben, können wir sie dann in einer Superklasse haben und Unterklassen verwenden, die die tatsächlichen POJOs von "content" sind?
Nima G
14

@ BrianRoachs Lösung ist die richtige Lösung. Es ist erwähnenswert, dass Sie in dem speziellen Fall, in dem Sie verschachtelte benutzerdefinierte Objekte haben, die beide eine benutzerdefinierte benötigen TypeAdapter, die TypeAdapterbei der neuen Instanz von GSON registrieren müssen , da sonst die zweite TypeAdapterniemals aufgerufen wird. Dies liegt daran, dass wir eine neue GsonInstanz in unserem benutzerdefinierten Deserializer erstellen .

Zum Beispiel, wenn Sie den folgenden json hatten:

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

Und Sie wollten, dass dieser JSON den folgenden Objekten zugeordnet wird:

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

class SubContent
{
    public String baz;
}

Sie müssten die SubContent's registrieren TypeAdapter. Um robuster zu sein, können Sie Folgendes tun:

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

    }
}

und dann erstelle es so:

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

Dies kann leicht auch für den verschachtelten "Inhalts" -Fall verwendet werden, indem einfach eine neue Instanz von MyDeserializermit Nullwerten übergeben wird.

KMarlow
quelle
1
Aus welchem ​​Paket stammt "Typ"? Es gibt eine Million Pakete mit der Klasse "Typ". Danke dir.
Kyle Bridenstine
2
@ Mr.Tea Es wird seinjava.lang.reflect.Type
aidan
1
Wo ist die SubContentDeserializer-Klasse? @KMarlow
LogronJ
10

Etwas spät, aber hoffentlich hilft das jemandem.

Erstellen Sie einfach die folgende 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();
    }
}

und fügen Sie es Ihrem GSON-Builder hinzu:

.registerTypeAdapterFactory(new ItemTypeAdapterFactory());

oder

 yourGsonBuilder.registerTypeAdapterFactory(new ItemTypeAdapterFactory());
Matin Petrulak
quelle
Genau so sehe ich aus. Weil ich viele Typen mit "Daten" -Knoten umwickelt habe und nicht jedem TypAdapter hinzufügen kann. Vielen Dank!
Sergey Irisov
@ SergeyIrisov Gern geschehen. Sie können diese Antwort abstimmen, damit sie höher wird :)
Matin Petrulak
Wie übergebe ich mehrere jsonElement? wie ich habe content, content1etc.
Sathish Kumar J
Ich denke, Ihre Back-End-Entwickler sollten die Struktur ändern und keine Inhalte weitergeben, content1 ... Was ist der Vorteil dieses Ansatzes?
Matin Petrulak
Danke dir! Dies ist die perfekte Antwort. @Marin Petrulak: Der Vorteil ist, dass dieses Formular für Änderungen zukunftssicher ist. "Inhalt" ist der Antwortinhalt. In Zukunft werden möglicherweise neue Felder wie "version", "lastUpdated", "sessionToken" usw. angezeigt. Wenn Sie Ihren Antwortinhalt nicht zuvor verpackt haben, werden Sie in Ihrem Code auf eine Reihe von Problemumgehungen stoßen, um sich an die neue Struktur anzupassen.
Muetzenflo
7

Hatte vor ein paar Tagen das gleiche Problem. Ich habe dieses Problem mit der Response-Wrapper-Klasse und dem RxJava-Transformator gelöst, was meiner Meinung nach eine recht flexible Lösung ist:

Verpackung:

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

Benutzerdefinierte Ausnahme zum Auslösen, wenn der Status nicht OK ist:

public class ApiException extends RuntimeException {
    private final String reason;

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

    public String getReason() {
        return apiError;
    }
}

Empfangstransformator:

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

Anwendungsbeispiel:

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

Mein Thema: Retrofit 2 RxJava - Gson - "Globale" Deserialisierung, Antworttyp ändern

Rafakob
quelle
Wie sieht MyPojo aus?
IgorGanapolsky
1
@IgorGanapolsky MyPojo kann aussehen, wie Sie wollen. Es sollte mit Ihren Inhaltsdaten übereinstimmen, die von einem Server abgerufen wurden. Die Struktur dieser Klasse sollte an Ihren Serialisierungskonverter (Gson, Jackson usw.) angepasst werden.
Rafakob
@rafakob kannst du mir auch bei meinem Problem helfen? Es fällt mir schwer, auf einfachste Weise ein Feld in meinem verschachtelten JSON zu finden. Hier ist meine Frage: stackoverflow.com/questions/56501897/…
6

In Fortsetzung von Brians Idee könnte es nützlich sein, die Deserialisierung zu verallgemeinern, da wir fast immer viele REST-Ressourcen mit jeweils eigener Wurzel haben:

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

    }
}

Um dann die Beispielnutzlast von oben zu analysieren, können wir den GSON-Deserializer registrieren:

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

Eine bessere Lösung könnte dies sein ..

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

Definieren Sie dann Ihren Service wie folgt.

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

Gemäß der Antwort von @Brian Roach und @rafakob habe ich dies folgendermaßen gemacht

Json Antwort vom Server

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

Gemeinsame Datenhandlerklasse

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

    @SerializedName("code")
    public int code;

    @SerializedName("message")
    public String reason;

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

Benutzerdefinierter Serializer

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

      }
}

Gson-Objekt

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

API-Anruf

 @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
quelle
2

Dies ist die gleiche Lösung wie bei @AYarulin, es wird jedoch davon ausgegangen, dass der Klassenname der JSON-Schlüsselname ist. Auf diese Weise müssen Sie nur den Klassennamen übergeben.

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

    }
}

Um dann die Beispielnutzlast von oben zu analysieren, können wir den GSON-Deserializer registrieren. Dies ist problematisch, da bei dem Schlüssel zwischen Groß- und Kleinschreibung unterschieden wird. Daher muss die Groß- und Kleinschreibung des Klassennamens mit der Groß- und Kleinschreibung des JSON-Schlüssels übereinstimmen.

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

Hier ist eine Kotlin-Version, die auf den Antworten von Brian Roach und AYarulin basiert.

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
quelle
1

In meinem Fall würde sich der "Inhalts" -Schlüssel für jede Antwort ändern. Beispiel:

// 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 solchen Fällen habe ich eine ähnliche Lösung wie oben aufgeführt verwendet, musste sie jedoch optimieren. Sie können das Wesentliche hier sehen . Es ist etwas zu groß, um es hier auf SOF zu veröffentlichen.

Die Anmerkung @InnerKey("content")wird verwendet und der Rest des Codes soll die Verwendung mit Gson erleichtern.

Varun Achar
quelle
Können Sie mir auch bei meiner Frage helfen? Haben Sie ein hrd Zeit, ein Feld von einem verschachtelten json auf einfachste Weise zu erhalten: stackoverflow.com/questions/56501897/…
0

Vergessen Sie nicht @SerializedNameund @ExposeAnmerkungen für alle Klassenmitglieder und Mitglieder der Inneren Klasse, die von GSON am meisten von JSON deserialisiert wurden.

Schauen Sie sich https://stackoverflow.com/a/40239512/1676736 an

Sagte Abolfazl Fatemi
quelle
0

Eine weitere einfache Lösung:

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