Получить вложенный объект JSON с помощью GSON с помощью модификации

111

Я использую API из своего приложения для Android, и все ответы JSON выглядят так:

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

Проблема заключается в том, что все мои POJOs есть status, reasonполя, так и внутри contentполя является реальным POJO я хочу.

Есть ли способ создать собственный конвертер Gson, чтобы всегда извлекать contentполе, чтобы модификация возвращала соответствующий POJO?

mikelar
источник
sites.google.com/site/gson/…
Брайан Роуч,
Я прочитал документ, но не понимаю, как это сделать ... :( Я не понимаю, как запрограммировать код для решения моей проблемы
микелар
Мне любопытно, почему вы просто не отформатируете свой класс POJO для обработки этих результатов статуса.
jj.

Ответы:

168

Вы должны написать собственный десериализатор, который возвращает внедренный объект.

Допустим, ваш JSON:

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

Тогда у вас будет ContentPOJO:

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

Затем вы пишете десериализатор:

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

    }
}

Теперь, если вы создадите с Gsonпомощью GsonBuilderи зарегистрируете десериализатор:

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

Вы можете десериализовать свой JSON прямо на свой Content:

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

Изменить, чтобы добавить из комментариев:

Если у вас есть разные типы сообщений, но все они имеют поле «content», вы можете сделать десериализатор универсальным, выполнив следующие действия:

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

    }
}

Вам просто нужно зарегистрировать экземпляр для каждого из ваших типов:

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

Когда вы вызываете .fromJson()тип, переносится в десериализатор, поэтому он должен работать для всех ваших типов.

И, наконец, при создании экземпляра Retrofit:

Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(url)
                .addConverterFactory(GsonConverterFactory.create(gson))
                .build();
Брайан Роуч
источник
1
Ух ты, здорово! Спасибо! : D Есть ли способ обобщить решение, чтобы мне не нужно было создавать один JsonDeserializer для каждого типа ответа?
mikelar
1
Это потрясающе! Следует изменить одно: Gson gson = new GsonBuilder (). Create (); Вместо Gson gson = new GsonBuilder (). Build (); Тому есть два примера.
Нельсон Осаки
7
@feresr вы можете позвонить setConverter(new GsonConverter(gson))в RestAdapter.Builderкласс Retrofit
akhy
2
@BrianRoach, спасибо, хороший ответ .. я должен зарегистрироваться Person.classи List<Person>.class/ Person[].classс отдельным десериализатором?
akhy 04
2
Есть ли возможность получить "статус" и "причину" тоже? Например, если все запросы возвращают их, можем ли мы иметь их в суперклассе и использовать подклассы, которые являются фактическими POJO из «содержимого»?
Nima G
14

Решение @BrianRoach - правильное решение. Стоит отметить, что в особом случае, когда у вас есть вложенные настраиваемые объекты, для которых требуется настраиваемый объект TypeAdapter, вы должны зарегистрировать его TypeAdapterс новым экземпляром GSON , иначе второй TypeAdapterникогда не будет вызван. Это потому, что мы создаем новый Gsonэкземпляр внутри нашего настраиваемого десериализатора.

Например, если у вас был следующий json:

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

И вы хотели, чтобы этот JSON был сопоставлен со следующими объектами:

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

class SubContent
{
    public String baz;
}

Вам нужно будет зарегистрировать SubContentфайл TypeAdapter. Для большей надежности вы можете сделать следующее:

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

    }
}

а затем создайте его так:

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

Это можно легко использовать для случая вложенного «содержимого», просто передав новый экземпляр MyDeserializerс нулевыми значениями.

KMarlow
источник
1
Из какого пакета "Тип"? Есть миллион пакетов, содержащих класс «Тип». Спасибо.
Кайл Брайденстайн
2
@ Mr.Tea Это будетjava.lang.reflect.Type
aidan
1
Где находится класс SubContentDeserializer? @KMarlow
LogronJ
10

Немного поздно, но, надеюсь, это кому-то поможет.

Просто создайте следующий 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();
    }
}

и добавьте его в свой конструктор GSON:

.registerTypeAdapterFactory(new ItemTypeAdapterFactory());

или

 yourGsonBuilder.registerTypeAdapterFactory(new ItemTypeAdapterFactory());
Матин Петрулак
источник
Я выгляжу именно так. Потому что у меня есть много типов, обернутых узлом «данные», и я не могу добавить TypeAdapter к каждому из них. Спасибо!
Сергей Ирисов
@SergeyIrisov пожалуйста. Вы можете проголосовать за этот ответ, чтобы он стал выше :)
Matin Petrulak
Как пройти несколько jsonElement?. как я есть content, content1и т.д.
Sathish Кумар J
Я думаю, что ваши серверные разработчики должны изменить структуру и не передавать контент, content1 ... В чем преимущество этого подхода?
Матин Петрулак
Спасибо! Это идеальный ответ. @Marin Petrulak: Преимущество в том, что эта форма рассчитана на изменения в будущем. «контент» - это контент ответа. В будущем в них могут появиться новые поля, такие как «версия», «lastUpdated», «sessionToken» и так далее. Если вы не обернули содержимое ответа заранее, вы столкнетесь с рядом обходных путей в своем коде, чтобы адаптироваться к новой структуре.
muetzenflo
7

Была такая же проблема пару дней назад. Я решил эту проблему, используя класс-оболочку ответа и преобразователь RxJava, что, на мой взгляд, является довольно гибким решением:

Упаковочный:

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

Пользовательское исключение, которое нужно выбросить, когда статус не в порядке:

public class ApiException extends RuntimeException {
    private final String reason;

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

    public String getReason() {
        return apiError;
    }
}

Трансформатор 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;
            });
}

Пример использования:

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

Моя тема: Retrofit 2 RxJava - Gson - "Глобальная" десериализация, изменение типа ответа

rafakob
источник
Как выглядит MyPojo?
Игорь Ганапольский
1
@IgorGanapolsky MyPojo может выглядеть так, как вы хотите. Он должен соответствовать данным вашего контента, полученным с сервера. Структура этого класса должна быть адаптирована к вашему преобразователю сериализации (Gson, Jackson и т. Д.).
rafakob
@rafakob, ты тоже можешь помочь мне с моей проблемой? С трудом пытаюсь получить поле в моем вложенном json как можно проще. вот мой вопрос: stackoverflow.com/questions/56501897/…
6

Продолжая идею Брайана, поскольку у нас почти всегда есть много ресурсов REST, каждый из которых имеет собственный корень, было бы полезно обобщить десериализацию:

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

    }
}

Затем, чтобы проанализировать образец полезной нагрузки сверху, мы можем зарегистрировать десериализатор GSON:

Gson gson = new GsonBuilder()
    .registerTypeAdapter(Content.class, new RestDeserializer<>(Content.class, "content"))
    .build();
AYarulin
источник
3

Лучшим решением может быть это ..

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

Затем определите свою службу следующим образом.

Observable<ApiResponse<YourClass>> updateDevice(..);
Федерико Дж Фарина
источник
3

Согласно ответу @Brian Roach и @rafakob, я сделал это следующим образом

Json ответ от сервера

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

Класс обработчика общих данных

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

    @SerializedName("code")
    public int code;

    @SerializedName("message")
    public String reason;

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

Пользовательский сериализатор

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

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

Вызов 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

Это то же решение, что и @AYarulin, но предполагается, что имя класса - это имя ключа JSON. Таким образом, вам нужно передать только имя класса.

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

    }
}

Затем, чтобы проанализировать образец полезной нагрузки сверху, мы можем зарегистрировать десериализатор GSON. Это проблематично, так как ключ чувствителен к регистру, поэтому регистр имени класса должен соответствовать регистру ключа JSON.

Gson gson = new GsonBuilder()
.registerTypeAdapter(Content.class, new RestDeserializer<>(Content.class))
.build();
Барри MSIH
источник
2

Вот версия Котлина, основанная на ответах Брайана Роуча и А.Ярулина.

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
источник
1

В моем случае ключ «содержимого» будет изменяться для каждого ответа. Пример:

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

В таких случаях я использовал аналогичное решение, указанное выше, но мне пришлось его настроить. Вы можете увидеть суть здесь . Он слишком велик, чтобы размещать его здесь, на SOF.

Используется аннотация, @InnerKey("content")а остальной код предназначен для облегчения ее использования с Gson.

Варун Ачар
источник
Не могли бы вы также помочь с моим вопросом. Получите hrd раз, получая поле из вложенного json самым простым способом: stackoverflow.com/questions/56501897/…
0

Не забывайте @SerializedNameи @Exposeаннотации для всех членов класса и членов внутреннего класса, которые наиболее десериализованы из JSON с помощью GSON.

Посмотрите https://stackoverflow.com/a/40239512/1676736

Сайед Аболфазл Фатеми
источник
0

Еще одно простое решение:

JsonObject parsed = (JsonObject) new JsonParser().parse(jsonString);
Content content = gson.fromJson(parsed.get("content"), Content.class);
Вадим
источник