Dapatkan objek JSON bersarang dengan GSON menggunakan retrofit

111

Saya menggunakan API dari aplikasi android saya, dan semua tanggapan JSON seperti ini:

{
    'status': 'OK',
    'reason': 'Everything was fine',
    'content': {
         < some data here >
}

Masalahnya adalah bahwa semua POJO saya memiliki status, reasonbidang, dan di dalam contentbidang tersebut adalah POJO asli yang saya inginkan.

Apakah ada cara untuk membuat konverter khusus Gson untuk selalu mengekstrak contentbidang, jadi retrofit mengembalikan POJO yang sesuai?

mikelar
sumber
Saya membaca dokumen tetapi saya tidak mengerti bagaimana melakukannya ... :( Saya tidak menyadari bagaimana memprogram kode untuk menyelesaikan masalah saya
mikelar
Saya ingin tahu mengapa Anda tidak hanya memformat kelas POJO Anda untuk menangani hasil status tersebut.
jj.

Jawaban:

168

Anda akan menulis deserializer khusus yang mengembalikan objek yang disematkan.

Misalkan JSON Anda adalah:

{
    "status":"OK",
    "reason":"some reason",
    "content" : 
    {
        "foo": 123,
        "bar": "some value"
    }
}

Anda kemudian akan memiliki ContentPOJO:

class Content
{
    public int foo;
    public String bar;
}

Kemudian Anda menulis deserializer:

class MyDeserializer implements JsonDeserializer<Content>
{
    @Override
    public Content deserialize(JsonElement je, Type type, JsonDeserializationContext jdc)
        throws JsonParseException
    {
        // Get the "content" element from the parsed JSON
        JsonElement content = je.getAsJsonObject().get("content");

        // Deserialize it. You use a new instance of Gson to avoid infinite recursion
        // to this deserializer
        return new Gson().fromJson(content, Content.class);

    }
}

Sekarang jika Anda membuat Gsondengan GsonBuilderdan mendaftarkan deserializer:

Gson gson = 
    new GsonBuilder()
        .registerTypeAdapter(Content.class, new MyDeserializer())
        .create();

Anda dapat menghapus derialisasi JSON langsung ke Content:

Content c = gson.fromJson(myJson, Content.class);

Edit untuk menambahkan dari komentar:

Jika Anda memiliki jenis pesan yang berbeda tetapi semuanya memiliki kolom "konten", Anda dapat membuat Deserializer generik dengan melakukan:

class MyDeserializer<T> implements JsonDeserializer<T>
{
    @Override
    public T deserialize(JsonElement je, Type type, JsonDeserializationContext jdc)
        throws JsonParseException
    {
        // Get the "content" element from the parsed JSON
        JsonElement content = je.getAsJsonObject().get("content");

        // Deserialize it. You use a new instance of Gson to avoid infinite recursion
        // to this deserializer
        return new Gson().fromJson(content, type);

    }
}

Anda hanya perlu mendaftarkan sebuah instance untuk setiap tipe Anda:

Gson gson = 
    new GsonBuilder()
        .registerTypeAdapter(Content.class, new MyDeserializer<Content>())
        .registerTypeAdapter(DiffContent.class, new MyDeserializer<DiffContent>())
        .create();

Ketika Anda memanggil .fromJson()tipe dibawa ke deserializer, jadi itu harus bekerja untuk semua tipe Anda.

Dan terakhir saat membuat instance Retrofit:

Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(url)
                .addConverterFactory(GsonConverterFactory.create(gson))
                .build();
Brian Roach
sumber
1
wow, bagus sekali! Terima kasih! : D Apakah ada cara untuk menggeneralisasi solusi sehingga saya tidak perlu membuat satu JsonDeserializer untuk setiap jenis respons?
mikelar
1
Ini luar biasa! Satu hal yang perlu diubah: Gson gson = new GsonBuilder (). Create (); Alih-alih Gson gson = new GsonBuilder (). Build (); Ada dua contoh tentang ini.
Nelson Osacky
7
@feresr Anda dapat menelepon setConverter(new GsonConverter(gson))di RestAdapter.Builderkelas Retrofit
akhy
2
@ BrianRoach terima kasih, jawaban yang bagus .. haruskah saya mendaftar Person.classdan List<Person>.class/ Person[].classdengan Deserializer terpisah?
akhy
2
Adakah kemungkinan untuk mendapatkan "status" dan "alasan" juga? Misalnya jika semua permintaan mengembalikannya, dapatkah kita memilikinya di kelas super dan menggunakan subkelas yang merupakan POJO sebenarnya dari "konten"?
Nima G
14

Solusi @ BrianRoach adalah solusi yang tepat. Perlu dicatat bahwa dalam kasus khusus di mana Anda memiliki objek kustom bersarang bahwa kedua kebutuhan kustom TypeAdapter, Anda harus mendaftarkan TypeAdapterdengan contoh baru dari Gson , jika kedua TypeAdaptertidak akan pernah disebut. Ini karena kami membuat Gsoninstance baru di dalam deserializer kustom kami.

Misalnya, jika Anda memiliki json berikut:

{
    "status": "OK",
    "reason": "some reason",
    "content": {
        "foo": 123,
        "bar": "some value",
        "subcontent": {
            "useless": "field",
            "data": {
                "baz": "values"
            }
        }
    }
}

Dan Anda ingin JSON ini dipetakan ke objek berikut:

class MainContent
{
    public int foo;
    public String bar;
    public SubContent subcontent;
}

class SubContent
{
    public String baz;
}

Anda akan perlu untuk mendaftarkan SubContent's TypeAdapter. Agar lebih kuat, Anda bisa melakukan hal berikut:

public class MyDeserializer<T> implements JsonDeserializer<T> {
    private final Class mNestedClazz;
    private final Object mNestedDeserializer;

    public MyDeserializer(Class nestedClazz, Object nestedDeserializer) {
        mNestedClazz = nestedClazz;
        mNestedDeserializer = nestedDeserializer;
    }

    @Override
    public T deserialize(JsonElement je, Type type, JsonDeserializationContext jdc) throws JsonParseException {
        // Get the "content" element from the parsed JSON
        JsonElement content = je.getAsJsonObject().get("content");

        // Deserialize it. You use a new instance of Gson to avoid infinite recursion
        // to this deserializer
        GsonBuilder builder = new GsonBuilder();
        if (mNestedClazz != null && mNestedDeserializer != null) {
            builder.registerTypeAdapter(mNestedClazz, mNestedDeserializer);
        }
        return builder.create().fromJson(content, type);

    }
}

dan kemudian buat seperti ini:

MyDeserializer<Content> myDeserializer = new MyDeserializer<Content>(SubContent.class,
                    new SubContentDeserializer());
Gson gson = new GsonBuilder().registerTypeAdapter(Content.class, myDeserializer).create();

Ini dapat dengan mudah digunakan untuk kasus "konten" bertingkat juga dengan hanya meneruskan contoh baru MyDeserializerdengan nilai null.

KMarlow
sumber
1
Dari paket apa "Jenis" itu? Ada sejuta paket yang berisi kelas "Type". Terima kasih.
Kyle Bridenstine
2
@ Mr. Teh Ini akanjava.lang.reflect.Type
aidan
1
Di manakah kelas SubContentDeserializer? @KKIndonesia
Logistik
10

Agak terlambat tapi semoga ini bisa membantu seseorang.

Cukup buat TypeAdapterFactory berikut.

    public class ItemTypeAdapterFactory implements TypeAdapterFactory {

      public <T> TypeAdapter<T> create(Gson gson, final TypeToken<T> type) {

        final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
        final TypeAdapter<JsonElement> elementAdapter = gson.getAdapter(JsonElement.class);

        return new TypeAdapter<T>() {

            public void write(JsonWriter out, T value) throws IOException {
                delegate.write(out, value);
            }

            public T read(JsonReader in) throws IOException {

                JsonElement jsonElement = elementAdapter.read(in);
                if (jsonElement.isJsonObject()) {
                    JsonObject jsonObject = jsonElement.getAsJsonObject();
                    if (jsonObject.has("content")) {
                        jsonElement = jsonObject.get("content");
                    }
                }

                return delegate.fromJsonTree(jsonElement);
            }
        }.nullSafe();
    }
}

dan tambahkan ke pembuat GSON Anda:

.registerTypeAdapterFactory(new ItemTypeAdapterFactory());

atau

 yourGsonBuilder.registerTypeAdapterFactory(new ItemTypeAdapterFactory());
Matin Petrulak
sumber
Ini persis seperti yang saya lihat. Karena saya memiliki banyak tipe yang dibungkus dengan node "data" dan saya tidak dapat menambahkan TypeAdapter ke masing-masing. Terima kasih!
Sergey Irisov
@SergeyIrisov sama-sama. Anda dapat memberikan suara untuk jawaban ini agar semakin tinggi :)
Matin Petrulak
Bagaimana cara lulus multipel jsonElement?. seperti yang saya miliki content, content1dll.
Sathish Kumar J
Saya pikir pengembang back-end Anda harus mengubah struktur dan tidak melewatkan konten, konten1 ... Apa keuntungan dari pendekatan ini?
Matin Petrulak
Terima kasih! Inilah jawaban yang tepat. @ Marin Petrulak: Keuntungannya adalah formulir ini tahan perubahan di masa mendatang. "konten" adalah konten tanggapan. Di masa mendatang, mereka mungkin datang ke kolom baru seperti "versi", "lastUpdated", "sessionToken", dan seterusnya. Jika Anda tidak membungkus konten respons Anda sebelumnya, Anda akan mengalami banyak solusi dalam kode Anda untuk beradaptasi dengan struktur baru.
muetzenflo
7

Mengalami masalah yang sama beberapa hari lalu. Saya telah menyelesaikan ini menggunakan kelas pembungkus respons dan transformator RxJava, yang menurut saya solusi yang cukup fleksibel:

Pembungkus:

public class ApiResponse<T> {
    public String status;
    public String reason;
    public T content;
}

Pengecualian khusus untuk dilempar, ketika status tidak OK:

public class ApiException extends RuntimeException {
    private final String reason;

    public ApiException(String reason) {
        this.reason = reason;
    }

    public String getReason() {
        return apiError;
    }
}

Trafo Rx:

protected <T> Observable.Transformer<ApiResponse<T>, T> applySchedulersAndExtractData() {
    return observable -> observable
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .map(tApiResponse -> {
                if (!tApiResponse.status.equals("OK"))
                    throw new ApiException(tApiResponse.reason);
                else
                    return tApiResponse.content;
            });
}

Contoh penggunaan:

// Call definition:
@GET("/api/getMyPojo")
Observable<ApiResponse<MyPojo>> getConfig();

// Call invoke:
webservice.getMyPojo()
        .compose(applySchedulersAndExtractData())
        .subscribe(this::handleSuccess, this::handleError);


private void handleSuccess(MyPojo mypojo) {
    // handle success
}

private void handleError(Throwable t) {
    getView().showSnackbar( ((ApiException) throwable).getReason() );
}

Topik saya: Retrofit 2 RxJava - Gson - Deserialisasi "Global", ubah jenis respons

rafakob
sumber
Seperti apa tampilan MyPojo?
IgorGanapolsky
1
@IgorGanapolsky MyPojo dapat tampil sesuka Anda. Ini harus cocok dengan data konten Anda yang diambil dari server. Struktur kelas ini harus disesuaikan dengan konverter serialisasi Anda (Gson, Jackson dll.).
rafakob
@rafakob dapatkah Anda membantu saya mengatasi masalah saya juga? Mengalami kesulitan mencoba mendapatkan bidang di json bersarang saya dengan cara yang sesederhana mungkin. inilah pertanyaan saya: stackoverflow.com/questions/56501897/…
6

Melanjutkan ide Brian, karena kita hampir selalu memiliki banyak resource REST dengan akarnya masing-masing, mungkin berguna untuk menggeneralisasi deserialisasi:

 class RestDeserializer<T> implements JsonDeserializer<T> {

    private Class<T> mClass;
    private String mKey;

    public RestDeserializer(Class<T> targetClass, String key) {
        mClass = targetClass;
        mKey = key;
    }

    @Override
    public T deserialize(JsonElement je, Type type, JsonDeserializationContext jdc)
            throws JsonParseException {
        JsonElement content = je.getAsJsonObject().get(mKey);
        return new Gson().fromJson(content, mClass);

    }
}

Kemudian untuk mem-parsing sample payload dari atas, kita dapat mendaftarkan deserializer GSON:

Gson gson = new GsonBuilder()
    .registerTypeAdapter(Content.class, new RestDeserializer<>(Content.class, "content"))
    .build();
AYarulin
sumber
3

Solusi yang lebih baik bisa jadi ini ..

public class ApiResponse<T> {
    public T data;
    public String status;
    public String reason;
}

Kemudian, tentukan layanan Anda seperti ini ..

Observable<ApiResponse<YourClass>> updateDevice(..);
Federico J Farina
sumber
3

Sesuai jawaban @Brian Roach dan @rafakob saya melakukan ini dengan cara berikut

Tanggapan Json dari server

{
  "status": true,
  "code": 200,
  "message": "Success",
  "data": {
    "fullname": "Rohan",
    "role": 1
  }
}

Kelas penangan data umum

public class ApiResponse<T> {
    @SerializedName("status")
    public boolean status;

    @SerializedName("code")
    public int code;

    @SerializedName("message")
    public String reason;

    @SerializedName("data")
    public T content;
}

Serializer khusus

static class MyDeserializer<T> implements JsonDeserializer<T>
{
     @Override
      public T deserialize(JsonElement je, Type type, JsonDeserializationContext jdc)
                    throws JsonParseException
      {
          JsonElement content = je.getAsJsonObject();

          // Deserialize it. You use a new instance of Gson to avoid infinite recursion
          // to this deserializer
          return new Gson().fromJson(content, type);

      }
}

Objek Gson

Gson gson = new GsonBuilder()
                    .registerTypeAdapter(ApiResponse.class, new MyDeserializer<ApiResponse>())
                    .create();

Panggilan api

 @FormUrlEncoded
 @POST("/loginUser")
 Observable<ApiResponse<Profile>> signIn(@Field("email") String username, @Field("password") String password);

restService.signIn(username, password)
                .observeOn(AndroidSchedulers.mainThread())
                .subscribeOn(Schedulers.io())
                .subscribe(new Observer<ApiResponse<Profile>>() {
                    @Override
                    public void onCompleted() {
                        Log.i("login", "On complete");
                    }

                    @Override
                    public void onError(Throwable e) {
                        Log.i("login", e.toString());
                    }

                    @Override
                    public void onNext(ApiResponse<Profile> response) {
                         Profile profile= response.content;
                         Log.i("login", profile.getFullname());
                    }
                });
Rohan Pawar
sumber
2

Ini adalah solusi yang sama dengan @AYarulin tetapi anggaplah nama kelasnya adalah nama kunci JSON. Dengan cara ini Anda hanya perlu memasukkan nama Kelas.

 class RestDeserializer<T> implements JsonDeserializer<T> {

    private Class<T> mClass;
    private String mKey;

    public RestDeserializer(Class<T> targetClass) {
        mClass = targetClass;
        mKey = mClass.getSimpleName();
    }

    @Override
    public T deserialize(JsonElement je, Type type, JsonDeserializationContext jdc)
            throws JsonParseException {
        JsonElement content = je.getAsJsonObject().get(mKey);
        return new Gson().fromJson(content, mClass);

    }
}

Kemudian untuk mengurai sample payload dari atas, kita bisa melakukan registrasi deserializer GSON. Ini bermasalah karena Kunci peka huruf besar / kecil, jadi huruf besar-kecil nama kelas harus cocok dengan huruf besar / kecil dari kunci JSON.

Gson gson = new GsonBuilder()
.registerTypeAdapter(Content.class, new RestDeserializer<>(Content.class))
.build();
Barry MSIH
sumber
2

Berikut versi Kotlin berdasarkan jawaban Brian Roach dan AYarulin.

class RestDeserializer<T>(targetClass: Class<T>, key: String?) : JsonDeserializer<T> {
    val targetClass = targetClass
    val key = key

    override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): T {
        val data = json!!.asJsonObject.get(key ?: "")

        return Gson().fromJson(data, targetClass)
    }
}
RamwiseMatt
sumber
1

Dalam kasus saya, kunci "konten" akan berubah untuk setiap respons. Contoh:

// Root is hotel
{
  status : "ok",
  statusCode : 200,
  hotels : [{
    name : "Taj Palace",
    location : {
      lat : 12
      lng : 77
    }

  }, {
    name : "Plaza", 
    location : {
      lat : 12
      lng : 77
    }
  }]
}

//Root is city

{
  status : "ok",
  statusCode : 200,
  city : {
    name : "Vegas",
    location : {
      lat : 12
      lng : 77
    }
}

Dalam kasus seperti itu saya menggunakan solusi serupa seperti yang tercantum di atas tetapi harus mengubahnya. Anda dapat melihat intinya di sini . Agak terlalu besar untuk diposting di sini di SOF.

Anotasi @InnerKey("content")digunakan dan kode lainnya adalah untuk memfasilitasi penggunaannya dengan Gson.

Varun Achar
sumber
Bisakah Anda membantu dengan pertanyaan saya juga. Dapatkan kesempatan untuk mendapatkan bidang dari json bersarang dengan cara yang paling sederhana: stackoverflow.com/questions/56501897/…
0

Solusi sederhana lainnya:

JsonObject parsed = (JsonObject) new JsonParser().parse(jsonString);
Content content = gson.fromJson(parsed.get("content"), Content.class);
Vadzim
sumber