it-swarm.com.de

Verschachtelte JSON-Objekte mit GSON mithilfe von Retrofit abrufen

Ich verwende eine API von 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, reason-Feld haben und dass innerhalb des content-Felds das echte POJO ist, das ich möchte.

Gibt es eine Möglichkeit, einen benutzerdefinierten Konverter von Gson zu erstellen, um immer das content-Feld zu extrahieren, so dass das entsprechende POJO nachgerüstet wird?

96
mikelar

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

Nehmen wir an, Ihr JSON ist:

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

Sie hätten dann eine Content POJO:

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 eine Gson mit GsonBuilder erstellen und den Deserializer registrieren:

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

Sie können Ihre JSON direkt zu Ihrer Content deserialisieren:

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

Bearbeiten, um aus Kommentaren hinzuzufügen:  

Wenn Sie verschiedene Arten von Nachrichten haben, aber alle über das Feld "Inhalt" verfügen, können Sie den Deserializer als generisch definieren, 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() aufrufen, wird der Typ in den Deserializer übernommen, sodass er für alle Ihre Typen funktionieren sollte. 

Und zum Schluss beim Erstellen einer Retrofit-Instanz:

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

Die Lösung von @ BrianRoach ist die richtige Lösung. Es ist erwähnenswert, dass Sie in dem Sonderfall, in dem Sie benutzerdefinierte Objekte verschachtelt haben, die beide ein benutzerdefiniertes TypeAdapter benötigen, das TypeAdapter mit der new-Instanz von GSON registrieren müssen, andernfalls wird das zweite TypeAdapter nie aufgerufen. Dies liegt daran, dass wir eine neue Gson-Instanz in unserem benutzerdefinierten Deserializer erstellen.

Wenn Sie beispielsweise 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 diese 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 von TypeAdapter registrieren. 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 wie folgt erstellen:

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

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

15
KMarlow

Etwas spät, aber hoffentlich wird dies jemandem helfen.

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());
9
Matin Petrulak

Bringt Brian fort, da wir fast immer viele REST -Ressourcen mit eigenen Wurzeln haben, könnte es nützlich sein, die Deserialisierung zu verallgemeinern:

 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 die Nutzdaten von oben zu analysieren, können wir den GSON-Deserialisierer registrieren:

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

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

Wrapper:

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

Benutzerdefinierte Ausnahmebedingung, 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;
    }
}

Rx Transformator:

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

Verwendungsbeispiel:

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

6
rafakob

In meiner Antwort von @Brian Roach und @Frafakob habe ich dies auf folgende Weise getan

Json Antwort vom Server

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

Allgemeine Datenbehandlungsklasse

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());
                    }
                });
3
Rohan Pawar

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

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

Dann definieren Sie Ihren Service so ..

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

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)
    }
}
2
RamwiseMatt

Dies ist die gleiche Lösung wie @AYarulin, es wird jedoch angenommen, 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 die Nutzdaten von oben zu analysieren, können wir den GSON-Deserialisierer registrieren. Dies ist problematisch, da bei dem Schlüssel die Groß- und Kleinschreibung beachtet wird. Daher muss der Fall des Klassennamens mit dem Fall des JSON-Schlüssels übereinstimmen. 

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

In meinem Fall würde sich der Schlüssel "Inhalt" 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 diese jedoch anpassen. Sie können den Gist hier sehen. Es ist etwas zu groß, um es hier auf SOF zu posten. 

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

1
Varun Achar

Eine andere einfache Lösung:

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

Vergessen Sie nicht die @SerializedName- und @Expose-Anmerkungen für alle Klassenmitglieder und Mitglieder der inneren Klasse, die von GSON am meisten von JSON deserialisiert wurden.

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