Gson benutzerdefinierter Seralizer für eine Variable (von vielen) in einem Objekt mit TypeAdapter

96

Ich habe viele einfache Beispiele für die Verwendung eines benutzerdefinierten TypeAdapters gesehen. Das hilfreichste war Class TypeAdapter<T>. Aber das hat meine Frage noch nicht beantwortet.

Ich möchte die Serialisierung eines einzelnen Felds im Objekt anpassen und den Standard-Gson-Mechanismus den Rest erledigen lassen.

Zu Diskussionszwecken können wir diese Klassendefinition als Klasse des Objekts verwenden, das ich serialisieren möchte. Ich möchte, dass Gson die ersten beiden Klassenmitglieder sowie alle exponierten Mitglieder der Basisklasse serialisiert, und ich möchte eine benutzerdefinierte Serialisierung für das unten gezeigte dritte und letzte Klassenmitglied durchführen.

public class MyClass extends SomeClass {

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

Antworten:

131

Dies ist eine großartige Frage, da sie etwas isoliert, das einfach sein sollte, aber tatsächlich viel Code erfordert.

Schreiben Sie zunächst eine Zusammenfassung TypeAdapterFactory, die Ihnen Hooks zum Ändern der ausgehenden Daten gibt. In diesem Beispiel wird eine neue API in Gson 2.2 verwendet getDelegateAdapter(), mit der Sie den Adapter nachschlagen können, den Gson standardmäßig verwenden würde. Die Delegatenadapter sind äußerst praktisch, wenn Sie nur das Standardverhalten optimieren möchten. Und im Gegensatz zu vollständigen benutzerdefinierten Adaptern bleiben diese beim Hinzufügen und Entfernen von Feldern automatisch auf dem neuesten Stand.

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

Die obige Klasse verwendet die Standardserialisierung, um einen JSON-Baum (dargestellt durch JsonElement) abzurufen , und ruft dann die Hook-Methode beforeWrite()auf, damit die Unterklasse diesen Baum anpassen kann. Ähnliches gilt für die Deserialisierung mit afterRead().

Als nächstes unterteilen wir dies für das spezifische MyClassBeispiel. Zur Veranschaulichung füge ich der Karte eine synthetische Eigenschaft namens "Größe" hinzu, wenn sie serialisiert wird. Und aus Symmetriegründen werde ich es entfernen, wenn es deserialisiert ist. In der Praxis kann dies eine beliebige Anpassung sein.

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

Fügen Sie zum Schluss alles zusammen, indem Sie eine angepasste GsonInstanz erstellen , die den neuen Typadapter verwendet:

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

Die neuen Typen TypeAdapter und TypeAdapterFactory von Gson sind äußerst leistungsfähig, aber auch abstrakt und erfordern eine effektive Anwendung. Hoffentlich finden Sie dieses Beispiel hilfreich!

Jesse Wilson
quelle
@ Jesse Danke! Ohne Ihre Hilfe hätte ich das nie herausgefunden!
MountainX
Ich konnte nicht new MyClassTypeAdapterFactory()mit dem privaten Ctor instanziieren ...
MountainX
Ah, tut mir leid. Ich habe das alles in einer Datei gemacht.
Jesse Wilson
7
Dieser Mechansim (beforeWrite und afterRead) sollte Teil des GSon-Kerns sein. Vielen Dank!
Melanie
2
Ich verwende TypeAdapter, um Endlosschleifen aufgrund gegenseitiger Referenzierung zu vermeiden. Dies ist ein großartiger Mechanismus. Vielen Dank an Jesse, obwohl ich fragen möchte, ob Sie eine Idee haben, mit diesem Mechanismus den gleichen Effekt zu erzielen. Ich habe aber Dinge im Sinn Ich möchte auf Ihre Meinung hören .. danke!
Mohammed R. El-Khoudary
16

Es gibt einen anderen Ansatz. Wie Jesse Wilson sagt, soll das einfach sein. Und raten Sie mal, es ist einfach!

Wenn Sie JsonSerializerund JsonDeserializerfür Ihren Typ implementieren , können Sie die gewünschten Teile verarbeiten und für alles andere mit sehr wenig Code an Gson delegieren . Ich zitiere der Einfachheit halber aus der Antwort von @ Perception auf eine andere Frage. Weitere Informationen finden Sie in dieser Antwort:

In diesem Fall ist es besser, a JsonSerializerim Gegensatz zu a zu verwenden TypeAdapter, aus dem einfachen Grund, dass Serialisierer Zugriff auf ihren Serialisierungskontext haben.

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

Der Hauptvorteil davon (abgesehen von der Vermeidung komplizierter Problemumgehungen) besteht darin, dass Sie weiterhin andere Typadapter und benutzerdefinierte Serializer nutzen können, die möglicherweise im Hauptkontext registriert wurden. Beachten Sie, dass die Registrierung von Serialisierern und Adaptern genau denselben Code verwendet.

Ich werde jedoch anerkennen, dass Jesses Ansatz besser aussieht, wenn Sie häufig Felder in Ihrem Java-Objekt ändern. Es ist ein Kompromiss zwischen Benutzerfreundlichkeit und Flexibilität. Treffen Sie Ihre Wahl.

Vicky Chijwani
quelle
1
Dadurch werden nicht alle anderen Felder an valuegson
Wesley
10

Mein Kollege erwähnte auch die Verwendung der @JsonAdapterAnmerkung

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

Die Seite wurde hierher verschoben: https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/annotations/JsonAdapter.html

Beispiel:

 private static final class Gadget {
   @JsonAdapter(UserJsonAdapter2.class)
   final User user;
   Gadget(User user) {
       this.user = user;
   }
 }
dazza5000
quelle
1
Dies funktioniert ziemlich gut für meinen Anwendungsfall. Vielen Dank.
Neoklosch
1
Hier ist ein WebArchive-Link, da das Original jetzt tot ist: web.archive.org/web/20180119143212/https://google.github.io/…
Floating Sunfish