Seralizer personnalisé Gson pour une variable (parmi plusieurs) dans un objet à l'aide de TypeAdapter

96

J'ai vu de nombreux exemples simples d'utilisation d'un TypeAdapter personnalisé. Le plus utile a été Class TypeAdapter<T>. Mais cela n'a pas encore répondu à ma question.

Je souhaite personnaliser la sérialisation d'un seul champ dans l'objet et laisser le mécanisme Gson par défaut s'occuper du reste.

À des fins de discussion, nous pouvons utiliser cette définition de classe comme classe de l'objet que je souhaite sérialiser. Je veux laisser Gson sérialiser les deux premiers membres de la classe ainsi que tous les membres exposés de la classe de base, et je veux faire une sérialisation personnalisée pour le troisième et dernier membre de classe illustré ci-dessous.

public class MyClass extends SomeClass {

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

Réponses:

131

C'est une excellente question car elle isole quelque chose qui devrait être facile mais qui nécessite en fait beaucoup de code.

Pour commencer, écrivez un résumé TypeAdapterFactoryqui vous donne des crochets pour modifier les données sortantes. Cet exemple utilise une nouvelle API dans Gson 2.2 appelée getDelegateAdapter()qui vous permet de rechercher l'adaptateur que Gson utiliserait par défaut. Les adaptateurs délégués sont extrêmement pratiques si vous souhaitez simplement modifier le comportement standard. Et contrairement aux adaptateurs de type personnalisés complets, ils resteront à jour automatiquement lorsque vous ajoutez et supprimez des champs.

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 classe ci-dessus utilise la sérialisation par défaut pour obtenir une arborescence JSON (représentée par JsonElement), puis appelle la méthode hook beforeWrite()pour permettre à la sous-classe de personnaliser cette arborescence. De même pour la désérialisation avec afterRead().

Ensuite, nous sous-classons ceci pour l' MyClassexemple spécifique . Pour illustrer, j'ajouterai une propriété synthétique appelée «taille» à la carte lorsqu'elle est sérialisée. Et pour la symétrie, je le supprimerai lorsqu'il sera désérialisé. En pratique, cela pourrait être n'importe quelle personnalisation.

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

Enfin, rassemblez le tout en créant une Gsoninstance personnalisée qui utilise le nouvel adaptateur de type:

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

Les nouveaux types TypeAdapter et TypeAdapterFactory de Gson sont extrêmement puissants, mais ils sont également abstraits et nécessitent de la pratique pour être utilisés efficacement. J'espère que vous trouverez cet exemple utile!

Jesse Wilson
la source
@Jesse Merci! Je n'aurais jamais compris cela sans votre aide!
MountainX
Je n'ai pas pu instancier new MyClassTypeAdapterFactory()avec le ctor privé ...
MountainX
Ah, désolé pour ça. J'ai fait tout cela dans un seul fichier.
Jesse Wilson
7
Ce mécanisme (beforeWrite et afterRead) devrait faire partie du noyau GSon. Merci!
Melanie
2
J'utilise TypeAdapter pour éviter des boucles infinies en raison du référencement mutuel .. c'est un excellent mécanisme merci @Jesse bien que j'aimerais vous demander si vous avez une idée d'obtenir le même effet avec ce mécanisme .. J'ai des choses en tête mais Je veux écouter ton avis .. merci!
Mohammed R. El-Khoudary le
16

Il y a une autre approche à cela. Comme le dit Jesse Wilson, cela est censé être facile. Et devinez quoi, il est facile!

Si vous implémentez JsonSerializeret JsonDeserializerpour votre type, vous pouvez gérer les parties que vous souhaitez et déléguer à Gson pour tout le reste , avec très peu de code. Je cite la réponse de @ Perception à une autre question ci-dessous pour plus de commodité, voir cette réponse pour plus de détails:

Dans ce cas, il vaut mieux utiliser a JsonSerializerplutôt que a TypeAdapter, pour la simple raison que les sérialiseurs ont accès à leur contexte de sérialisation.

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

Le principal avantage de ceci (en plus d'éviter des solutions de contournement compliquées) est que vous pouvez toujours bénéficier d'autres adaptateurs de type et sérialiseurs personnalisés qui pourraient avoir été enregistrés dans le contexte principal. Notez que l'enregistrement des sérialiseurs et des adaptateurs utilise exactement le même code.

Cependant, je reconnais que l'approche de Jesse est meilleure si vous allez fréquemment modifier des champs dans votre objet Java. C'est un compromis entre facilité d'utilisation et flexibilité, faites votre choix.

Vicky Chijwani
la source
1
Cela ne permet pas de déléguer tous les autres champs valueà gson
Wesley
10

Mon collègue a également mentionné l'utilisation de l' @JsonAdapterannotation

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

La page a été déplacée ici: https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/annotations/JsonAdapter.html

Exemple:

 private static final class Gadget {
   @JsonAdapter(UserJsonAdapter2.class)
   final User user;
   Gadget(User user) {
       this.user = user;
   }
 }
dazza5000
la source
1
Cela fonctionne plutôt bien pour mon cas d'utilisation. Merci beaucoup.
Neoklosch
1
Voici un lien WebArchive car l'original est maintenant mort: web.archive.org/web/20180119143212/https
Floating Sunfish