Seralizator niestandardowy Gson dla jednej zmiennej (z wielu) w obiekcie przy użyciu TypeAdapter

96

Widziałem wiele prostych przykładów użycia niestandardowego TypeAdapter. Najbardziej pomocny był Class TypeAdapter<T>. Ale to jeszcze nie odpowiedział na moje pytanie.

Chcę dostosować serializację pojedynczego pola w obiekcie i pozwolić domyślnemu mechanizmowi Gson zająć się resztą.

Dla celów dyskusji możemy użyć tej definicji klasy jako klasy obiektu, który chcę serializować. Chcę pozwolić Gsonowi na serializację pierwszych dwóch elementów członkowskich klasy, a także wszystkich ujawnionych elementów członkowskich klasy bazowej, i chcę wykonać niestandardową serializację dla trzeciego i ostatniego elementu członkowskiego klasy, pokazanego poniżej.

public class MyClass extends SomeClass {

@Expose private HashMap<String, MyObject1> lists;
@Expose private HashMap<String, MyObject2> sources;
private LinkedHashMap<String, SomeClass> customSerializeThis;
    [snip]
}
MountainX
źródło

Odpowiedzi:

131

To świetne pytanie, ponieważ wyodrębnia coś, co powinno być łatwe, ale w rzeczywistości wymaga dużo kodu.

Na początek napisz streszczenie TypeAdapterFactory, które pozwoli Ci zmodyfikować dane wychodzące. W tym przykładzie użyto nowego interfejsu API w Gson 2.2 o nazwie, getDelegateAdapter()który umożliwia wyszukanie adaptera, którego Gson używałby domyślnie. Adaptery delegatów są bardzo przydatne, jeśli chcesz po prostu dostosować standardowe zachowanie. W przeciwieństwie do w pełni niestandardowych adapterów będą one automatycznie aktualizowane podczas dodawania i usuwania pól.

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

Powyższa klasa używa domyślnej serializacji, aby uzyskać drzewo JSON (reprezentowane przez JsonElement), a następnie wywołuje metodę hook, beforeWrite()aby umożliwić podklasie dostosowanie tego drzewa. Podobnie w przypadku deserializacji z afterRead().

Następnie podklasujemy to dla konkretnego MyClassprzykładu. Aby zilustrować, dodam syntetyczną właściwość o nazwie „size” do mapy, gdy jest ona serializowana. I dla symetrii usunę go, gdy zostanie zdeserializowany. W praktyce może to być dowolna personalizacja.

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

Na koniec połącz to wszystko razem, tworząc niestandardową Gsoninstancję, która korzysta z adaptera nowego typu:

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

Nowe typy TypeAdapter i TypeAdapterFactory firmy Gson są niezwykle zaawansowane, ale są również abstrakcyjne i wymagają praktyki, aby skutecznie używać. Mamy nadzieję, że ten przykład okaże się przydatny!

Jesse Wilson
źródło
@Jesse Dziękuję! Nigdy bym tego nie zrozumiał bez Twojej pomocy!
MountainX
Nie udało mi się utworzyć instancji new MyClassTypeAdapterFactory()z prywatnym ctor ...
MountainX,
Ach, przepraszam za to. Zrobiłem to wszystko w jednym pliku.
Jesse Wilson
7
Ten mechansim (beforeWrite i afterRead) powinien być częścią rdzenia GSon. Dzięki!
Melanie,
2
Używam TypeAdapter, aby uniknąć nieskończonych pętli z powodu wzajemnych odwołań .. to świetny mechanizm, dziękuję @Jesse, chociaż chciałbym zapytać, czy masz pomysł na osiągnięcie tego samego efektu za pomocą tego mechanizmu. Mam pewne rzeczy na myśli, ale Chcę posłuchać Twojej opinii… dziękuję!
Mohammed R. El-Khoudary
16

Jest inne podejście do tego. Jak mówi Jesse Wilson, ma to być łatwe. I wiecie co, to jest proste!

Jeśli zaimplementujesz JsonSerializeri JsonDeserializerdla swojego typu, możesz obsłużyć żądane części i delegować do Gson na wszystko inne , z bardzo małą ilością kodu. Cytuję z odpowiedzią @ percepcji na innym pytaniu poniżej dla wygody zobaczyć tę odpowiedź więcej szczegółów:

W tym przypadku lepiej jest użyć a JsonSerializerzamiast a TypeAdapter, z tego prostego powodu, że serializatory mają dostęp do swojego kontekstu serializacji.

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

Główną zaletą tego (oprócz unikania skomplikowanych obejść) jest to, że nadal można korzystać z innych adapterów typu i niestandardowych serializatorów, które mogły zostać zarejestrowane w głównym kontekście. Zwróć uwagę, że rejestracja serializatorów i adapterów używa dokładnie tego samego kodu.

Jednak przyznaję, że podejście Jessego wygląda lepiej, jeśli często zamierzasz modyfikować pola w obiekcie Java. To kompromis między łatwością obsługi a elastycznością, wybierz.

Vicky Chijwani
źródło
1
To nie valuedaje delegacji wszystkich innych dziedzin na gson
Wesley
10

Mój kolega również wspomniał o zastosowaniu @JsonAdapteradnotacji

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

Strona została przeniesiona tutaj: https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/annotations/JsonAdapter.html

Przykład:

 private static final class Gadget {
   @JsonAdapter(UserJsonAdapter2.class)
   final User user;
   Gadget(User user) {
       this.user = user;
   }
 }
dazza5000
źródło
1
Działa to całkiem dobrze w moim przypadku użycia. Wielkie dzięki.
Neoklosch,
1
Oto link do WebArchive, ponieważ oryginał nie żyje: web.archive.org/web/20180119143212/https://google.github.io/…
Floating Sunfish