Отримайте вкладений об’єкт 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

Мені цікаво, чому ви не просто відформатували ваш клас 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);

Редагувати, щоб додати до коментарів:

Якщо у вас є різні типи повідомлень, але всі вони містять поле "вміст", ви можете зробити Deserializer загальним шляхом:

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 для кожного типу відповідей?
мікляр

1
Це дивно! Що потрібно змінити: Gson gson = новий GsonBuilder (). Create (); Замість Gson gson = новий GsonBuilder (). Build (); Є два випадки цього.
Нельсон Осакі,

7
@feresr ви можете зателефонувати setConverter(new GsonConverter(gson))в RestAdapter.Builderклас Retrofit
akhy

2
@BrianRoach дякую, приємна відповідь .. я повинен зареєструватися Person.classта List<Person>.class/ Person[].classз відокремленим Deserializer?
ахій

2
Будь-яка можливість отримати "статус" і "причина" теж? Наприклад, якщо всі запити повертають їх, чи можемо ми мати їх у суперкласі та використовувати підкласи, які є власне POJO з "вмісту"?
Німа Г

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«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з нульовими значеннями.


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

Це саме те, на що я дивлюся. Оскільки у мене дуже багато типів, обгорнутих вузлом "data", і я не можу додати TypeAdapter до кожного з них. Дякую!
Сергій Ірисов

@SergeyIrisov, ласкаво просимо. Ви можете проголосувати за цю відповідь, щоб вона
стала

Як пройти кілька jsonElement? як у мене content, content1і т. д.
Sathish Kumar J

Я думаю, що ваші бек-енди повинні змінити структуру і не передавати зміст, зміст1 ... У чому перевага такого підходу?
Матін Петрулак

Дякую! Це ідеальна відповідь. @Marin Petrulak: Перевага полягає в тому, що ця форма є надійною для змін у майбутньому. "зміст" - це вміст відповіді. В майбутньому вони можуть з’явитися нові поля, такі як "версія", "lastUp updated", "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 - "Глобальна" десеріалізація, зміна типу відповіді


Як виглядає 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();

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

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

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

Ще одне просте рішення:

JsonObject parsed = (JsonObject) new JsonParser().parse(jsonString);
Content content = gson.fromJson(parsed.get("content"), Content.class);
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.