Uzyskaj zagnieżdżony obiekt JSON za pomocą GSON za pomocą modernizacji

111

Korzystam z interfejsu API z mojej aplikacji na Androida, a wszystkie odpowiedzi JSON są takie:

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

Problemem jest to, że wszyscy moi POJOs mieć status, reasonpola, a wewnątrz contentpola jest prawdziwym POJO chcę.

Czy istnieje sposób na stworzenie niestandardowego konwertera Gson, który wyodrębnia zawsze contentpole, więc retrofit zwraca odpowiednie POJO?

mikelar
źródło
Czytałem dokument, ale nie wiem, jak to zrobić ... :( Nie wiem, jak zaprogramować kod, aby rozwiązać mój problem
mikelar 14.04.14
Jestem ciekawy, dlaczego nie sformatowałbyś swojej klasy POJO po prostu do obsługi tych wyników statusu.
jj.

Odpowiedzi:

168

Możesz napisać niestandardowy deserializator, który zwraca osadzony obiekt.

Powiedzmy, że Twój JSON to:

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

Miałbyś wtedy ContentPOJO:

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

Następnie piszesz deserializator:

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

    }
}

Teraz, jeśli skonstruować Gsonz GsonBuilderi zarejestrować Deserializator:

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

Możesz deserializować swój JSON bezpośrednio na Content:

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

Edytuj, aby dodać z komentarzy:

Jeśli masz różne typy wiadomości, ale wszystkie mają pole „zawartość”, możesz ustawić deserializator jako ogólny, wykonując:

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

    }
}

Musisz tylko zarejestrować instancję dla każdego ze swoich typów:

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

Po wywołaniu .fromJson()typ jest przenoszony do deserializatora, więc powinien działać dla wszystkich typów.

I wreszcie podczas tworzenia instancji typu Retrofit:

Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(url)
                .addConverterFactory(GsonConverterFactory.create(gson))
                .build();
Brian Roach
źródło
1
Wow to świetnie! dzięki! : D Czy istnieje sposób na uogólnienie rozwiązania, aby nie musieć tworzyć jednego JsonDeserializer dla każdego typu odpowiedzi?
mikelar
1
To jest niesamowite! Jedna rzecz do zmiany: Gson gson = new GsonBuilder (). Create (); Zamiast Gson gson = new GsonBuilder (). Build (); Istnieją dwa przykłady tego.
Nelson Osacky
7
@feresr możesz zadzwonić setConverter(new GsonConverter(gson))w RestAdapter.Builderklasie Retrofit
akhy
2
@BrianRoach dzięki, miła odpowiedź .. czy powinienem się zarejestrować Person.classi List<Person>.class/ Person[].classz oddzielnym Deserializerem?
akhy
2
Czy jest jakaś możliwość uzyskania „statusu” i „powodu”? Na przykład, jeśli wszystkie żądania zwracają je, czy możemy je mieć w superklasie i używać podklas, które są rzeczywistymi obiektami POJO z „content”?
Nima G
14

Rozwiązanie @ BrianRoach jest właściwym rozwiązaniem. Warto zauważyć, że w szczególnym przypadku, gdy masz zagnieżdżone obiekty niestandardowe, które wymagają niestandardowego TypeAdapter, musisz zarejestrować TypeAdapterje w nowej instancji GSON , w przeciwnym razie druga TypeAdapternigdy nie zostanie wywołana. Dzieje się tak, ponieważ tworzymy nowyGson instancję w naszym niestandardowym deserializatorze.

Na przykład, jeśli masz następujący plik json:

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

I chciałeś, aby ten JSON został zmapowany do następujących obiektów:

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

class SubContent
{
    public String baz;
}

Musisz zarejestrować SubContentplik TypeAdapter. Aby być bardziej niezawodnym, możesz wykonać następujące czynności:

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

    }
}

a następnie utwórz go w ten sposób:

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

Można to z łatwością wykorzystać również w przypadku zagnieżdżonego przypadku „content”, po prostu przekazując nową instancję MyDeserializerz wartościami null.

KMarlow
źródło
1
Z jakiego pakietu pochodzi „Typ”? Istnieje milion paczek zawierających klasę „Typ”. Dziękuję Ci.
Kyle Bridenstine
2
@ Mr.Tea To będziejava.lang.reflect.Type
aidan
1
Gdzie jest klasa SubContentDeserializer? @KMarlow
LogronJ,
10

Trochę późno, ale mam nadzieję, że to komuś pomoże.

Po prostu utwórz następujący 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();
    }
}

i dodaj go do swojego kreatora GSON:

.registerTypeAdapterFactory(new ItemTypeAdapterFactory());

lub

 yourGsonBuilder.registerTypeAdapterFactory(new ItemTypeAdapterFactory());
Matin Petrulak
źródło
Dokładnie tak wyglądam. Ponieważ mam wiele typów opakowanych w węzeł „data” i nie mogę dodać TypeAdapter do każdego z nich. Dzięki!
Sergey Irisov
@SergeyIrisov nie ma za co. Możesz głosować na tę odpowiedź, żeby była wyżej :)
Matin Petrulak
Jak zdać wiele jsonElement? Tak jak ma content, content1itp
Sathish Kumar J
Myślę, że twoi back-end developerzy powinni zmienić strukturę i nie przekazywać content, content1 ... Jaka jest zaleta tego podejścia?
Matin Petrulak,
Dziękuję Ci! To jest doskonała odpowiedź. @Marin Petrulak: Zaletą jest to, że ten formularz jest przygotowany na przyszłe zmiany. „treść” to treść odpowiedzi. W przyszłości mogą pojawić się nowe pola, takie jak „version”, „lastUpdated”, „sessionToken” i tak dalej. Jeśli wcześniej nie opakowałeś treści odpowiedzi, napotkasz kilka obejść w swoim kodzie, aby dostosować się do nowej struktury.
muetzenflo
7

Miałem ten sam problem kilka dni temu. Rozwiązałem to za pomocą klasy opakowania odpowiedzi i transformatora RxJava, co moim zdaniem jest dość elastycznym rozwiązaniem:

Obwoluta:

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

Niestandardowy wyjątek do zgłoszenia, gdy stan nie jest OK:

public class ApiException extends RuntimeException {
    private final String reason;

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

    public String getReason() {
        return apiError;
    }
}

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

Przykładowe użycie:

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

Mój temat: Retrofit 2 RxJava - Gson - "Globalna" deserializacja, zmiana typu odpowiedzi

rafakob
źródło
Jak wygląda MyPojo?
Igor Ganapolsky
1
@IgorGanapolsky MyPojo może wyglądać tak, jak chcesz. Powinien pasować do danych zawartości pobranych z serwera. Struktura tej klasy powinna być dostosowana do twojego konwertera serializacji (Gson, Jackson itp.).
rafakob
@rafakob czy możesz mi również pomóc z moim problemem? Mam trudności z uzyskaniem pola w moim zagnieżdżonym pliku JSON w najprostszy możliwy sposób. oto moje pytanie: stackoverflow.com/questions/56501897/ ...
6

Kontynuując pomysł Briana, ponieważ prawie zawsze mamy wiele zasobów REST, z których każdy ma swój własny katalog główny, przydatne może być uogólnienie deserializacji:

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

    }
}

Następnie, aby przeanalizować przykładowy ładunek z góry, możemy zarejestrować deserializator GSON:

Gson gson = new GsonBuilder()
    .registerTypeAdapter(Content.class, new RestDeserializer<>(Content.class, "content"))
    .build();
AYarulin
źródło
3

Lepszym rozwiązaniem mogłoby być to ...

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

Następnie zdefiniuj swoją usługę w ten sposób.

Observable<ApiResponse<YourClass>> updateDevice(..);
Federico J Farina
źródło
3

Zgodnie z odpowiedzią @Brian Roach i @rafakob zrobiłem to w następujący sposób

Odpowiedź JSON z serwera

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

Wspólna klasa obsługi danych

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

    @SerializedName("code")
    public int code;

    @SerializedName("message")
    public String reason;

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

Niestandardowy serializator

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

      }
}

Obiekt Gson

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

Połączenie 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
źródło
2

To jest to samo rozwiązanie co @AYarulin, ale załóżmy, że nazwa klasy to nazwa klucza JSON. W ten sposób wystarczy podać nazwę klasy.

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

    }
}

Następnie, aby przeanalizować przykładowy ładunek z góry, możemy zarejestrować deserializator GSON. Jest to problematyczne, ponieważ w kluczu rozróżniana jest wielkość liter, więc wielkość liter w nazwie klasy musi odpowiadać wielkości liter w kluczu JSON.

Gson gson = new GsonBuilder()
.registerTypeAdapter(Content.class, new RestDeserializer<>(Content.class))
.build();
Barry MSIH
źródło
2

Oto wersja Kotlina oparta na odpowiedziach Briana Roacha i AYarulina.

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
źródło
1

W moim przypadku klucz „content” zmieniłby się dla każdej odpowiedzi. Przykład:

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

W takich przypadkach użyłem podobnego rozwiązania, jak wymienione powyżej, ale musiałem je poprawić. Możesz zobaczyć sedno tutaj . Jest trochę za duży, aby opublikować go tutaj w SOF.

Adnotacja @InnerKey("content")jest używana, a reszta kodu ma ułatwić jej użycie z Gson.

Varun Achar
źródło
Czy możesz również odpowiedzieć na moje pytanie. Miej hrd czas na pobranie pola z zagnieżdżonego pliku JSON w najprostszy możliwy sposób: stackoverflow.com/questions/56501897/ ...
0

Kolejne proste rozwiązanie:

JsonObject parsed = (JsonObject) new JsonParser().parse(jsonString);
Content content = gson.fromJson(parsed.get("content"), Content.class);
Vadzim
źródło