Seralizador personalizado de gson para una variable (de muchas) en un objeto usando TypeAdapter

96

He visto muchos ejemplos simples del uso de un TypeAdapter personalizado. El más útil ha sido Class TypeAdapter<T>. Pero eso aún no ha respondido a mi pregunta.

Quiero personalizar la serialización de un solo campo en el objeto y dejar que el mecanismo predeterminado de Gson se encargue del resto.

Para fines de discusión, podemos usar esta definición de clase como la clase del objeto que deseo serializar. Quiero permitir que Gson serialice los dos primeros miembros de la clase, así como todos los miembros expuestos de la clase base, y quiero hacer una serialización personalizada para el tercer y último miembro de la clase que se muestra a continuación.

public class MyClass extends SomeClass {

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

Respuestas:

131

Esta es una gran pregunta porque aísla algo que debería ser fácil pero que en realidad requiere mucho código.

Para comenzar, escriba un resumen TypeAdapterFactoryque le proporcione ganchos para modificar los datos salientes. Este ejemplo usa una nueva API en Gson 2.2 llamada getDelegateAdapter()que le permite buscar el adaptador que Gson usaría por defecto. Los adaptadores de delegado son extremadamente útiles si solo desea modificar el comportamiento estándar. Y a diferencia de los adaptadores de tipo personalizados completos, se mantendrán actualizados automáticamente a medida que agregue y elimine campos.

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

La clase anterior usa la serialización predeterminada para obtener un árbol JSON (representado por JsonElement), y luego llama al método hook beforeWrite()para permitir que la subclase personalice ese árbol. De manera similar para la deserialización con afterRead().

A continuación, subclasificamos esto para el MyClassejemplo específico . Para ilustrar, agregaré una propiedad sintética llamada 'tamaño' al mapa cuando esté serializado. Y por simetría, lo eliminaré cuando esté deserializado. En la práctica, esto podría ser cualquier personalización.

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

Finalmente, júntelo todo creando una Gsoninstancia personalizada que use el nuevo adaptador de tipo:

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

Los nuevos tipos TypeAdapter y TypeAdapterFactory de Gson son extremadamente poderosos, pero también son abstractos y requieren práctica para usarlos de manera efectiva. ¡Esperamos que este ejemplo le resulte útil!

Jesse Wilson
fuente
@Jesse ¡Gracias! ¡Nunca me hubiera dado cuenta de esto sin tu ayuda!
MountainX
No pude crear una instancia new MyClassTypeAdapterFactory()con el ctor privado ...
MountainX
Ah, perdón por eso. Hice todo esto en un solo archivo.
Jesse Wilson
7
Ese mecanismo (beforeWrite y afterRead) debería ser parte del núcleo de GSon. ¡Gracias!
Melanie
2
Estoy usando TypeAdapter para evitar bucles infinitos debido a referencias mutuas ... este es un gran mecanismo, gracias @Jesse, aunque me gustaría preguntarle si tiene una idea de lograr el mismo efecto con este mecanismo ... Tengo algunas cosas en mente, pero Quiero escuchar tu opinión .. ¡gracias!
Mohammed R. El-Khoudary
16

Hay otro enfoque para esto. Como dice Jesse Wilson, se supone que esto es fácil. ¡Y adivinen qué, es fácil!

Si implementa JsonSerializery JsonDeserializerpara su tipo, puede manejar las partes que desee y delegar en Gson todo lo demás , con muy poco código. Cito la respuesta de @ Perception sobre otra pregunta a continuación para mayor comodidad, consulte esa respuesta para obtener más detalles:

En este caso, es mejor utilizar a JsonSerializeren lugar de a TypeAdapter, por la sencilla razón de que los serializadores tienen acceso a su contexto de serialización.

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

La principal ventaja de esto (además de evitar soluciones complicadas) es que aún puede aprovechar otros adaptadores de tipo y serializadores personalizados que podrían haberse registrado en el contexto principal. Tenga en cuenta que el registro de serializadores y adaptadores utiliza exactamente el mismo código.

Sin embargo, reconoceré que el enfoque de Jesse se ve mejor si con frecuencia va a modificar campos en su objeto Java. Es una compensación entre facilidad de uso y flexibilidad, elija su opción.

Vicky Chijwani
fuente
1
Esto no delega todos los demás campos en valuegson
Wesley
10

Mi colega también mencionó el uso de la @JsonAdapteranotación

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

La página se ha movido aquí: https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/annotations/JsonAdapter.html

Ejemplo:

 private static final class Gadget {
   @JsonAdapter(UserJsonAdapter2.class)
   final User user;
   Gadget(User user) {
       this.user = user;
   }
 }
dazza5000
fuente
1
Esto funciona bastante bien para mi caso de uso. Muchas gracias.
Neoklosch
1
Aquí hay un enlace de WebArchive porque el original ahora está muerto: web.archive.org/web/20180119143212/https://google.github.io/…
Floating Sunfish