Güçlendirme kullanarak GSON ile iç içe geçmiş JSON nesnesi edinin


111

Android uygulamamdan bir API kullanıyorum ve tüm JSON yanıtları şöyle:

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

Sorun şu ki, tüm POJO'larımın bir status, reasonalanları var ve contentalanın içinde istediğim gerçek POJO var.

Her zaman contentalanı çıkarmak için özel bir Gson dönüştürücüsü oluşturmanın herhangi bir yolu var mı , böylece iyileştirme uygun POJO'yu döndürür?



Belgeyi okudum ama nasıl yapacağımı bilmiyorum ... :(
Sorunumu

Neden POJO sınıfınızı bu durum sonuçlarını işleyecek şekilde biçimlendirmeyeceğinizi merak ediyorum.
jj.

Yanıtlar:


168

Gömülü nesneyi döndüren özel bir seri kaldırıcı yazarsınız.

Diyelim ki JSON'nuz:

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

Daha sonra bir ContentPOJO'nuz olur:

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

Daha sonra seri durumdan çıkarıcı yazarsınız:

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

    }
}

Şimdi bir Gsonwith oluşturur GsonBuilderve seriyi kaldırıcıyı kaydederseniz:

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

JSON'unuzun serisini kaldırıp doğrudan şunlara gidebilirsiniz Content:

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

Yorumlardan eklemek için düzenleyin:

Farklı türde mesajlarınız varsa ancak hepsinde "içerik" alanı varsa, aşağıdaki işlemleri yaparak Deserializer'ı jenerik yapabilirsiniz:

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

    }
}

Türlerinizden her biri için bir örnek kaydetmeniz yeterlidir:

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

.fromJson()Tür aradığınızda seri kaldırıcıya taşınır, bu nedenle tüm türleriniz için çalışmalıdır.

Ve son olarak Retrofit örneği oluştururken:

Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(url)
                .addConverterFactory(GsonConverterFactory.create(gson))
                .build();

1
vay bu harika! Teşekkürler! : D Her yanıt türü için bir JsonDeserializer oluşturmam gerekmemesi için çözümü genelleştirmenin herhangi bir yolu var mı?
mikelar

1
Bu harika! Değiştirilecek bir şey: Gson gson = new GsonBuilder (). Create (); Gson yerine gson = new GsonBuilder (). Build (); Bunun iki örneği var.
Nelson Osacky

7
@feresr setConverter(new GsonConverter(gson))Retrofit RestAdapter.Buildersınıfında arayabilirsin
akhy

2
@BrianRoach teşekkürler, güzel cevap .. kayıt olmalı mıyım Person.classve List<Person>.class/ Person[].classayrı Deserializer ile mi?
akhy

2
"Durum" ve "neden" de alma imkanı var mı? Örneğin, tüm istekler geri dönerse, onları bir süper sınıfta alabilir miyiz ve "içerikten" gerçek POJO'lar olan alt sınıfları kullanabilir miyiz?
Nima G

14

@ BrianRoach'ın çözümü doğru çözümdür. Her ikisinin de bir özelliğe ihtiyaç duyduğu iç içe geçmiş özel nesnelerinizin olduğu özel durumda , yeni GSON örneğineTypeAdapter kaydettirmeniz gerekir , aksi takdirde ikincisi asla çağrılmaz. Bunun nedeni , özel seri kaldırıcımızın içinde yeni bir örnek oluşturmamızdır .TypeAdapterTypeAdapterGson

Örneğin, aşağıdaki json'a sahipseniz:

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

Ve bu JSON'un aşağıdaki nesnelerle eşlenmesini istediniz:

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

class SubContent
{
    public String baz;
}

SubContent'Larını kaydetmeniz gerekir TypeAdapter. Daha sağlam olmak için şunları yapabilirsiniz:

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

    }
}

ve sonra şöyle oluşturun:

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

Bu, yuvalanmış "içerik" durumu için kolayca MyDeserializerboş değerlerle yeni bir örneğini ileterek de kullanılabilir .


1
"Tür" hangi paketten geliyor? "Tip" sınıfını içeren bir milyon paket var. Teşekkür ederim.
Kyle Bridenstine

2
@ Bay Çay Olacakjava.lang.reflect.Type
aidan

1
SubContentDeserializer sınıfı nerede? @KMarlow
LogronJ

10

Biraz geç ama umarım bu birine yardımcı olur.

Aşağıdaki TypeAdapterFactory'yi oluşturmanız yeterlidir.

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

ve GSON oluşturucunuza ekleyin:

.registerTypeAdapterFactory(new ItemTypeAdapterFactory());

veya

 yourGsonBuilder.registerTypeAdapterFactory(new ItemTypeAdapterFactory());

Bu tam olarak göründüğüm şey. Çünkü "veri" düğümü ile sarmalanmış birçok türüm var ve her birine TypeAdapter ekleyemiyorum. Teşekkürler!
Sergey Irisov

@SergeyIrisov hoş geldiniz. Bu cevaba oy verebilirsiniz, böylece yükselir :)
Matin Petrulak

Birden fazla nasıl geçilir jsonElement? i var gibi content, content1vb
Sathish Kumar J

Bence arka uç geliştiricileriniz yapıyı değiştirmeli ve içerik, içerik1 geçmemelidir ... Bu yaklaşımın avantajı nedir?
Matin Petrulak

Teşekkür ederim! Bu mükemmel cevap. @Marin Petrulak: Avantajı, bu formun değişiklikler için geleceğe hazır olmasıdır. "içerik", yanıt içeriğidir. Gelecekte "version", "lastUpdated", "sessionToken" ve benzeri gibi yeni alanlar gelebilir. Yanıt içeriğinizi önceden sarmadıysanız, yeni yapıya uyum sağlamak için kodunuzda bir dizi geçici çözümle karşılaşacaksınız.
muetzenflo

7

Birkaç gün önce aynı sorunu yaşadım. Bunu, oldukça esnek bir çözüm olduğunu düşündüğüm yanıt sarmalayıcı sınıfı ve RxJava trafosunu kullanarak çözdüm:

sarıcı:

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

Durum Tamam olmadığında atılacak özel istisna:

public class ApiException extends RuntimeException {
    private final String reason;

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

    public String getReason() {
        return apiError;
    }
}

Rx transformatörü:

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

Örnek kullanım:

// 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() );
}

Konum : Retrofit 2 RxJava - Gson - "Global" seriyi kaldırma, yanıt türünü değiştir


MyPojo neye benziyor?
IgorGanapolsky

1
@IgorGanapolsky MyPojo istediğiniz gibi görünebilir. Bir sunucudan alınan içerik verilerinizle eşleşmelidir. Bu sınıfın yapısı serileştirme dönüştürücünüze (Gson, Jackson vs.) göre ayarlanmalıdır.
rafakob

@rafakob benim sorunumda bana yardımcı olabilir misin? Mümkün olan en basit şekilde iç içe json'umda bir alan almaya çalışırken zor anlar yaşayın. sorum şu: stackoverflow.com/questions/56501897/…

6

Brian'ın fikrine devam edersek, neredeyse her zaman her biri kendi köküne sahip birçok REST kaynağımız olduğundan, seriyi kaldırma işlemini genelleştirmek faydalı olabilir:

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

    }
}

Daha sonra, örnek yükünü yukarıdan ayrıştırmak için, GSON deserializer'ı kaydedebiliriz:

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

3

Daha iyi bir çözüm bu olabilir ..

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

Ardından hizmetinizi şu şekilde tanımlayın ..

Observable<ApiResponse<YourClass>> updateDevice(..);

3

@ Brian Roach ve @rafakob'un cevabına göre bunu şu şekilde yaptım

Sunucudan Json yanıtı

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

Ortak veri işleyici sınıfı

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

    @SerializedName("code")
    public int code;

    @SerializedName("message")
    public String reason;

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

Özel serileştirici

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

      }
}

Gson nesnesi

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

API çağrısı

 @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());
                    }
                });

2

Bu, @AYarulin ile aynı çözümdür ancak sınıf adının JSON anahtar adı olduğunu varsayalım. Bu şekilde yalnızca Sınıf adını geçmeniz gerekir.

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

    }
}

Daha sonra örnek yükünü yukarıdan ayrıştırmak için, GSON seriyi kaldırıcıyı kaydedebiliriz. Anahtar büyük / küçük harfe duyarlı olduğundan bu sorunludur, bu nedenle sınıf adının büyük / küçük harf durumu JSON anahtarının büyük / küçük harfleriyle eşleşmelidir.

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

2

Brian Roach ve AYarulin'in cevaplarına dayanan bir Kotlin versiyonu.

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

1

Benim durumumda, "içerik" anahtarı her yanıt için değişecektir. Misal:

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

Bu gibi durumlarda, yukarıda listelendiğine benzer bir çözüm kullandım, ancak ince ayar yapmak zorunda kaldım. Burada özü görebilirsiniz . SOF'a burada göndermek için biraz fazla büyük.

Ek açıklama @InnerKey("content")kullanılır ve kodun geri kalanı Gson ile kullanımını kolaylaştırmak içindir.


Soruma da yardım edebilir misin? Mümkün olan en basit şekilde iç içe geçmiş bir


0

Başka bir basit çözüm:

JsonObject parsed = (JsonObject) new JsonParser().parse(jsonString);
Content content = gson.fromJson(parsed.get("content"), Content.class);
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.