Пользовательский серализатор 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]
}
MountainX
источник

Ответы:

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
Это не позволяет передать все остальные поля valueв gson
Уэсли,
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;
   }
 }
dazza5000
источник
1
Это очень хорошо работает для моего варианта использования. Большое спасибо.
Neoklosch
1
Вот ссылка на WebArchive, потому что оригинал уже мертв: web.archive.org/web/20180119143212/https://google.github.io/…
Floating Sunfish