Seralizzatore personalizzato Gson per una variabile (di molte) in un oggetto utilizzando TypeAdapter


96

Ho visto molti semplici esempi di utilizzo di un TypeAdapter personalizzato. Il più utile è stato Class TypeAdapter<T>. Ma questo non ha ancora risposto alla mia domanda.

Voglio personalizzare la serializzazione di un singolo campo nell'oggetto e lasciare che il meccanismo Gson predefinito si occupi del resto.

A scopo di discussione, possiamo usare questa definizione di classe come la classe dell'oggetto che desidero serializzare. Voglio che Gson serializzi i primi due membri della classe e tutti i membri esposti della classe base e voglio eseguire la serializzazione personalizzata per il terzo e ultimo membro della classe mostrato di seguito.

public class MyClass extends SomeClass {

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

Risposte:


131

Questa è un'ottima domanda perché isola qualcosa che dovrebbe essere facile ma che in realtà richiede molto codice.

Per iniziare, scrivi un abstract TypeAdapterFactoryche ti dia degli hook per modificare i dati in uscita. Questo esempio utilizza una nuova API in Gson 2.2 chiamata getDelegateAdapter()che consente di cercare l'adattatore che Gson userebbe per impostazione predefinita. Gli adattatori delegato sono estremamente utili se si desidera modificare il comportamento standard. E a differenza degli adattatori di tipo completamente personalizzati, rimarranno aggiornati automaticamente man mano che aggiungi e rimuovi i campi.

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 precedente utilizza la serializzazione predefinita per ottenere un albero JSON (rappresentato da JsonElement), quindi chiama il metodo hook beforeWrite()per consentire alla sottoclasse di personalizzare quell'albero. Allo stesso modo per la deserializzazione con afterRead().

Successivamente lo sottoclassiamo per l' MyClassesempio specifico . Per illustrare, aggiungerò una proprietà sintetica chiamata "size" alla mappa quando viene serializzata. E per la simmetria lo rimuoverò quando sarà deserializzato. In pratica questa potrebbe essere una qualsiasi personalizzazione.

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

Infine metti tutto insieme creando Gsonun'istanza personalizzata che utilizza il nuovo adattatore di tipo:

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

I nuovi tipi TypeAdapter e TypeAdapterFactory di Gson sono estremamente potenti, ma sono anche astratti e richiedono pratica per essere utilizzati in modo efficace. Spero che trovi utile questo esempio!


@ Jesse Grazie! Non l'avrei mai capito senza il tuo aiuto!
MountainX

Non sono riuscito a creare un'istanza new MyClassTypeAdapterFactory()con l'operatore privato ...
MountainX

Ah, mi dispiace per quello. Ho fatto tutto questo in un file.
Jesse Wilson

7
Quel Mechansim (beforeWrite e afterRead) dovrebbe far parte del core di GSon. Grazie!
Melanie

2
Sto usando TypeAdapter per evitare loop infiniti a causa di riferimenti reciproci .. questo è un ottimo meccanismo grazie @Jesse anche se vorrei chiederti se hai un'idea di ottenere lo stesso effetto con questo meccanismo .. Ho delle cose in mente ma Voglio ascoltare la tua opinione .. grazie!
Mohammed R. El-Khoudary,

16

C'è un altro approccio a questo. Come dice Jesse Wilson, dovrebbe essere facile. E indovina un po ', è facile!

Se implementi JsonSerializere JsonDeserializerper il tuo tipo, puoi gestire le parti che desideri e delegare a Gson per tutto il resto , con pochissimo codice. Sto citando la risposta di @ Perception su un'altra domanda qui sotto per comodità, vedi quella risposta per maggiori dettagli:

In questo caso è meglio usare a JsonSerializerinvece di a TypeAdapter, per il semplice motivo che i serializzatori hanno accesso al loro contesto di serializzazione.

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

Il vantaggio principale di questo (oltre a evitare complicate soluzioni alternative) è che puoi comunque sfruttare altri adattatori di tipo e serializzatori personalizzati che potrebbero essere stati registrati nel contesto principale. Notare che la registrazione di serializzatori e adattatori utilizza lo stesso identico codice.

Tuttavia, riconoscerò che l'approccio di Jesse sembra migliore se modificherai spesso i campi nel tuo oggetto Java. È un compromesso tra facilità d'uso e flessibilità, fai la tua scelta.


1
Questo non riesce a delegare tutti gli altri campi valuea gson
Wesley

10

Il mio collega ha anche menzionato l'uso @JsonAdapterdell'annotazione

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

La pagina è stata spostata qui: https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/annotations/JsonAdapter.html

Esempio:

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

1
Funziona abbastanza bene per il mio caso d'uso. Molte grazie.
Neoklosch

1
Ecco un collegamento WebArchive perché l'originale è ora morto: web.archive.org/web/20180119143212/https://google.github.io/…
Floating Sunfish
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.