it-swarm.com.ru

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

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

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

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

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

96
mikelar

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

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

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

Затем у вас будет Content POJO:

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

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

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

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();
151
Brian Roach

Решение @ 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's 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 с нулевыми значениями.

15
KMarlow

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

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

Продолжая идею Брайана, поскольку у нас почти всегда есть много 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();
7
AYarulin

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

Wrapper:

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

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

6
rafakob

Согласно ответу @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());
                    }
                });
3
Rohan Pawar

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

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

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

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

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

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

Это то же решение, что и @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();
2
Barry MSIH

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

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

1
Varun Achar

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

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

0
Sayed Abolfazl Fatemi