web-dev-qa-db-fra.com

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

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 3e et dernier membre de la classe 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]
}
90
MountainX

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é TypeAdapterFactory qui 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é complets, ils resteront à jour automatiquement lorsque vous ajouterez et supprimerez 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 de 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 cela pour l'exemple spécifique MyClass. Pour illustrer, j'ajouterai une propriété synthétique appelée "taille" à la carte lorsqu'elle sera sérialisée. Et pour la symétrie, je le supprimerai quand 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, assemblez le tout en créant une instance Gson 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 sont pratiques à utiliser efficacement. J'espère que cet exemple vous sera utile!

124
Jesse Wilson

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

Si vous implémentez JsonSerializer et JsonDeserializer pour 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 sur 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 un JsonSerializer plutôt qu'un TypeAdapter , pour la simple raison que les sérialiseurs ont accès à leur sérialisation le contexte.

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 cela (en plus d'éviter les solutions de contournement compliquées) est que vous pouvez toujours bénéficier d'autres adaptateurs de type et de 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 semble meilleure si vous allez fréquemment modifier des champs dans votre Java objet. C'est un compromis entre facilité d'utilisation et flexibilité, faites votre choix .

12
Vicky Chijwani

Mon collègue a également mentionné l'utilisation du @JsonAdapter annotation

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

Exemple:

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