Penyalur khusus Gson untuk satu variabel (dari banyak) dalam objek menggunakan TypeAdapter

96

Saya telah melihat banyak contoh sederhana menggunakan TypeAdapter khusus. Yang paling membantu adalah Class TypeAdapter<T>. Tapi itu belum menjawab pertanyaanku.

Saya ingin menyesuaikan serialisasi satu bidang dalam objek dan membiarkan mekanisme Gson default menangani sisanya.

Untuk tujuan diskusi, kita dapat menggunakan definisi kelas ini sebagai kelas dari objek yang ingin saya buat bersambung. Saya ingin agar Gson membuat serialisasi dua anggota kelas pertama serta semua anggota kelas dasar yang terpapar, dan saya ingin melakukan serialisasi khusus untuk anggota kelas ke-3 dan terakhir yang ditunjukkan di bawah ini.

public class MyClass extends SomeClass {

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

Jawaban:

131

Ini adalah pertanyaan yang bagus karena mengisolasi sesuatu yang seharusnya mudah tetapi sebenarnya membutuhkan banyak kode.

Untuk memulai, tulis abstrak TypeAdapterFactoryyang memberi Anda hook untuk mengubah data keluar. Contoh ini menggunakan API baru di Gson 2.2 yang disebut getDelegateAdapter()yang memungkinkan Anda untuk mencari adaptor yang akan digunakan Gson secara default. Adaptor delegasi sangat berguna jika Anda hanya ingin mengubah perilaku standar. Dan tidak seperti adaptor tipe khusus lengkap, mereka akan tetap terbarui secara otomatis saat Anda menambah dan menghapus bidang.

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

Kelas atas menggunakan serialisasi standar untuk mendapatkan pohon JSON (diwakili oleh JsonElement), dan kemudian memanggil metode kailbeforeWrite() untuk memungkinkan subclass menyesuaikan pohon itu. Demikian pula untuk deserialization dengan afterRead().

Selanjutnya kami membuat subkelas ini untuk MyClasscontoh spesifik . Untuk mengilustrasikan saya akan menambahkan properti sintetis yang disebut 'size' ke peta ketika berseri. Dan untuk simetri, saya akan menghapusnya saat deserialisasi. Dalam praktiknya, ini bisa berupa kustomisasi apa pun.

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

Terakhir, gabungkan semuanya dengan membuat Gsoninstance kustom yang menggunakan adaptor tipe baru:

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

Jenis baru TypeAdapter dan TypeAdapterFactory Gson sangat kuat, tetapi mereka juga abstrak dan membutuhkan latihan untuk digunakan secara efektif. Semoga contoh ini bermanfaat bagi Anda!

Jesse Wilson
sumber
@Jesse Terima kasih! Saya tidak akan pernah tahu ini tanpa bantuan Anda!
MountainX
Saya tidak dapat membuat instance new MyClassTypeAdapterFactory()dengan ctor pribadi ...
MountainX
Ah, maaf soal itu. Saya melakukan semua ini dalam satu file.
Jesse Wilson
7
Mekanisme itu (beforeWrite dan afterRead) harus menjadi bagian dari inti GSon. Terima kasih!
Melanie
2
Saya menggunakan TypeAdapter untuk menghindari loop tak terbatas karena saling mereferensikan .. ini adalah mekanisme yang bagus terima kasih @Jesse meskipun saya ingin bertanya apakah Anda memiliki gagasan untuk mencapai efek yang sama dengan mekanisme ini .. Saya punya beberapa pemikiran tetapi Saya ingin mendengarkan pendapat Anda .. terima kasih!
Mohammed R. El-Khoudary
16

Ada pendekatan lain untuk ini. Seperti yang dikatakan Jesse Wilson, ini seharusnya mudah. Dan coba tebak, itu adalah mudah!

Jika Anda mengimplementasikan JsonSerializerdan JsonDeserializeruntuk tipe Anda, Anda dapat menangani bagian-bagian yang Anda inginkan dan mendelegasikan ke Gson untuk yang lainnya , dengan kode yang sangat sedikit. Saya mengutip dari jawaban @ Persepsi tentang pertanyaan lain di bawah ini untuk kenyamanan, lihat jawaban itu untuk lebih jelasnya:

Dalam hal ini lebih baik menggunakan a JsonSerializerdaripada a TypeAdapter, karena alasan sederhana bahwa pembuat serial memiliki akses ke konteks serialisasi mereka.

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

Keuntungan utama dari ini (selain menghindari solusi yang rumit) adalah Anda masih dapat memanfaatkan adaptor tipe lain dan serializer kustom yang mungkin telah terdaftar dalam konteks utama. Perhatikan bahwa pendaftaran serializer dan adaptor menggunakan kode yang sama persis.

Namun, saya akan mengakui bahwa pendekatan Jesse terlihat lebih baik jika Anda sering mengubah bidang di objek Java Anda. Ini trade-off dari kemudahan penggunaan vs fleksibilitas, silakan pilih.

Vicky Chijwani
sumber
1
Ini gagal untuk mendelegasikan semua bidang lainnya valueke gson
Wesley
10

Rekan saya juga menyebutkan penggunaan @JsonAdapteranotasi

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

Halaman tersebut telah dipindahkan ke sini: https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/annotations/JsonAdapter.html

Contoh:

 private static final class Gadget {
   @JsonAdapter(UserJsonAdapter2.class)
   final User user;
   Gadget(User user) {
       this.user = user;
   }
 }
dazza5000
sumber
1
Ini bekerja dengan cukup baik untuk kasus penggunaan saya. Terima kasih banyak.
Neoklosch
1
Ini adalah tautan WebArchive karena aslinya sekarang sudah mati: web.archive.org/web/20180119143212/https://google.github.io/…
Floating Sunfish