Спеціальний сералізатор Gson для однієї змінної (з багатьох) в об'єкті за допомогою TypeAdapter


96

Я бачив безліч простих прикладів користувальницького TypeAdapter. Найбільш корисним було Class TypeAdapter<T>. Але це ще не відповіло на моє запитання.

Я хочу налаштувати серіалізацію одного поля в об'єкті, а про інше дозволити механізм Gson за замовчуванням.

Для цілей обговорення ми можемо використовувати це визначення класу як клас об'єкта, який я хочу серіалізувати. Я хочу дозволити Gson серіалізувати перших двох членів класу, а також усіх відкритих членів базового класу, і я хочу виконати власну серіалізацію для третього та останнього члена класу, показаного нижче.

public class MyClass extends SomeClass {

@Expose private HashMap<String, MyObject1> lists;
@Expose private HashMap<String, MyObject2> sources;
private LinkedHashMap<String, SomeClass> customSerializeThis;
    [snip]
}

Відповіді:


131

Це чудове питання, оскільки воно виділяє щось, що повинно бути простим, але насправді вимагає багато коду.

Для початку напишіть реферат, TypeAdapterFactoryякий дає вам змогу змінити вихідні дані. У цьому прикладі використовується новий API у Gson 2.2, що називається, getDelegateAdapter()що дозволяє шукати адаптер, який Gson використовував би за замовчуванням. Адаптери делегатів дуже зручні, якщо ви просто хочете налаштувати стандартну поведінку. І на відміну від повних адаптерів спеціального типу, вони будуть автоматично оновлюватися при додаванні та видаленні полів.

public abstract class CustomizedTypeAdapterFactory<C>
    implements TypeAdapterFactory {
  private final Class<C> customizedClass;

  public CustomizedTypeAdapterFactory(Class<C> customizedClass) {
    this.customizedClass = customizedClass;
  }

  @SuppressWarnings("unchecked") // we use a runtime check to guarantee that 'C' and 'T' are equal
  public final <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
    return type.getRawType() == customizedClass
        ? (TypeAdapter<T>) customizeMyClassAdapter(gson, (TypeToken<C>) type)
        : null;
  }

  private TypeAdapter<C> customizeMyClassAdapter(Gson gson, TypeToken<C> type) {
    final TypeAdapter<C> delegate = gson.getDelegateAdapter(this, type);
    final TypeAdapter<JsonElement> elementAdapter = gson.getAdapter(JsonElement.class);
    return new TypeAdapter<C>() {
      @Override public void write(JsonWriter out, C value) throws IOException {
        JsonElement tree = delegate.toJsonTree(value);
        beforeWrite(value, tree);
        elementAdapter.write(out, tree);
      }
      @Override public C read(JsonReader in) throws IOException {
        JsonElement tree = elementAdapter.read(in);
        afterRead(tree);
        return delegate.fromJsonTree(tree);
      }
    };
  }

  /**
   * Override this to muck with {@code toSerialize} before it is written to
   * the outgoing JSON stream.
   */
  protected void beforeWrite(C source, JsonElement toSerialize) {
  }

  /**
   * Override this to muck with {@code deserialized} before it parsed into
   * the application type.
   */
  protected void afterRead(JsonElement deserialized) {
  }
}

Вищезазначений клас використовує серіалізацію за замовчуванням, щоб отримати дерево JSON (представлене JsonElement), а потім викликає метод хука, beforeWrite()щоб дозволити підкласу налаштувати це дерево. Аналогічно для десеріалізації з afterRead().

Далі ми підкласуємо це для конкретного MyClassприкладу. Для ілюстрації я додаю на карту синтетичну властивість, що називається 'size', коли вона серіалізована. А для симетрії я її видалю, коли вона десеріалізована. На практиці це може бути будь-яка настройка.

private class MyClassTypeAdapterFactory extends CustomizedTypeAdapterFactory<MyClass> {
  private MyClassTypeAdapterFactory() {
    super(MyClass.class);
  }

  @Override protected void beforeWrite(MyClass source, JsonElement toSerialize) {
    JsonObject custom = toSerialize.getAsJsonObject().get("custom").getAsJsonObject();
    custom.add("size", new JsonPrimitive(custom.entrySet().size()));
  }

  @Override protected void afterRead(JsonElement deserialized) {
    JsonObject custom = deserialized.getAsJsonObject().get("custom").getAsJsonObject();
    custom.remove("size");
  }
}

Нарешті, все це разом, створивши спеціальний Gsonекземпляр, який використовує адаптер нового типу:

Gson gson = new GsonBuilder()
    .registerTypeAdapterFactory(new MyClassTypeAdapterFactory())
    .create();

Нові типи Gson TypeAdapter та TypeAdapterFactory надзвичайно потужні, але вони також абстрактні та вимагають практики для ефективного використання. Сподіваємось, ви знайдете цей приклад корисним!


@Jesse Дякую! Я б ніколи не зрозумів цього без вашої допомоги!
MountainX

Я не зміг створити екземпляр new MyClassTypeAdapterFactory()із приватним ctor ...
MountainX

Ах, вибачте за це. Я зробив все це в одному файлі.
Джессі Вілсон,

7
Цей механізм (beforeWrite і afterRead) повинен бути частиною ядра GSon. Дякую!
Мелані

2
Я використовую TypeAdapter, щоб уникнути нескінченних циклів через взаємне посилання .. це чудовий механізм, дякую @Jesse, хоча я хотів би запитати, чи маєте ви ідею досягнення того ж ефекту за допомогою цього механізму .. Я маю на увазі щось, але Хочу вислухати вашу думку .. дякую!
Мохаммед Р. Ель-Хударі

16

До цього існує інший підхід. Як говорить Джессі Вілсон, це повинно бути легко. І вгадайте , що це є просто!

Якщо ви реалізуєте JsonSerializerі JsonDeserializerдля свого типу, ви можете обробляти потрібні вам частини та делегувати Gson для всього іншого , з дуже невеликим кодом. Я цитую відповідь @ Perception на інше питання нижче для зручності, див. Цю відповідь для більш детальної інформації:

У цьому випадку краще використовувати a JsonSerializerна відміну від a TypeAdapter, з тієї простої причини, що серіалізатори мають доступ до свого контексту серіалізації.

public class PairSerializer implements JsonSerializer<Pair> {
    @Override
    public JsonElement serialize(final Pair value, final Type type,
            final JsonSerializationContext context) {
        final JsonObject jsonObj = new JsonObject();
        jsonObj.add("first", context.serialize(value.getFirst()));
        jsonObj.add("second", context.serialize(value.getSecond()));
        return jsonObj;
    }
}

Основною перевагою цього (крім уникнення складних обхідних шляхів) є те, що ви все ще можете скористатися іншими адаптерами типу та користувацькими серіалізаторами, які могли бути зареєстровані в основному контексті. Зверніть увагу, що для реєстрації серіалізаторів та адаптерів використовується точно такий самий код.

Однак я визнаю, що підхід Джессі виглядає краще, якщо ви часто збираєтеся змінювати поля у своєму об'єкті Java. Це компроміс між простотою використання та гнучкістю, зробіть свій вибір.


1
Не вдається делегувати всі інші поля valuegson
Уеслі,

10

Мій колега також згадав про використання @JsonAdapterанотації

https://google.github.io/gson/apidocs/com/google/gson/annotations/JsonAdapter.html

Сторінку було переміщено сюди: https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/annotations/JsonAdapter.html

Приклад:

 private static final class Gadget {
   @JsonAdapter(UserJsonAdapter2.class)
   final User user;
   Gadget(User user) {
       this.user = user;
   }
 }

1
Це працює досить добре для мого випадку використання. Дуже дякую.
Neoklosch

1
Ось посилання на WebArchive, оскільки оригінал уже мертвий: web.archive.org/web/20180119143212/https://google.github.io/…
Плаваюча
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.