รับวัตถุ JSON ที่ซ้อนกันด้วย GSON โดยใช้ชุดติดตั้งเพิ่มเติม


111

ฉันใช้ API จากแอป Android ของฉันและการตอบสนอง JSON ทั้งหมดเป็นดังนี้:

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

ปัญหาคือว่า POJOs ของฉันทุกคนมีstatus, reasonสาขาและภายในcontentสนามเป็นตัวจริง POJO ฉันต้องการ

มีวิธีใดบ้างในการสร้างตัวแปลง Gson ที่กำหนดเองเพื่อแยกcontentฟิลด์เสมอดังนั้นการติดตั้งเพิ่มจะส่งคืน POJO ที่เหมาะสม



ฉันอ่านเอกสาร แต่ไม่เห็นวิธีการทำ ... :( ฉันไม่รู้ว่าจะตั้งโปรแกรมโค้ดเพื่อแก้ปัญหาของฉันได้อย่างไร
mikelar

ฉันสงสัยว่าทำไมคุณไม่จัดรูปแบบคลาส POJO ของคุณเพื่อจัดการกับผลลัพธ์สถานะเหล่านั้น
jj.

คำตอบ:


168

คุณจะต้องเขียน deserializer แบบกำหนดเองที่ส่งคืนวัตถุฝังตัว

สมมติว่า JSON ของคุณคือ:

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

จากนั้นคุณจะมีContentPOJO:

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

จากนั้นคุณเขียน 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);

    }
}

ตอนนี้ถ้าคุณสร้างGsonด้วยGsonBuilderและลงทะเบียน deserializer นี้:

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

คุณสามารถยกเลิกการกำหนดค่า JSON ของคุณตรงไปที่Content:

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

แก้ไขเพื่อเพิ่มจากความคิดเห็น:

หากคุณมีข้อความประเภทต่างๆ แต่ทั้งหมดมีช่อง "เนื้อหา" คุณสามารถสร้าง Deserializer ทั่วไปได้โดยทำดังนี้

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

    }
}

คุณต้องลงทะเบียนอินสแตนซ์สำหรับแต่ละประเภทของคุณ:

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

เมื่อคุณเรียก.fromJson()ประเภทนั้นจะถูกนำไปยัง deserializer ดังนั้นจึงควรใช้ได้กับทุกประเภทของคุณ

และสุดท้ายเมื่อสร้างอินสแตนซ์ Retrofit:

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

1
ว้าวเยี่ยมมาก! ขอบคุณ! : D มีวิธีใดในการสรุปโซลูชันโดยที่ฉันไม่ต้องสร้าง JsonDeserializer หนึ่งตัวต่อการตอบสนองแต่ละประเภท
mikelar

1
สิ่งนี้ช่างมหัศจรรย์! สิ่งหนึ่งที่ต้องเปลี่ยนแปลง: Gson gson = new GsonBuilder (). create (); แทนที่จะเป็น Gson gson = GsonBuilder ใหม่ (). build (); มีสองกรณีนี้
Nelson Osacky

7
@feresr คุณสามารถโทรหาsetConverter(new GsonConverter(gson))ในRestAdapter.Builderชั้นเรียนของ Retrofit ได้
akhy

2
@BrianRoach ขอบคุณคำตอบที่ดี .. ฉันควรลงทะเบียนPerson.classและList<Person>.class/ Person[].classแยก Deserializer หรือไม่?
akhy

2
มีความเป็นไปได้ที่จะได้รับ "สถานะ" และ "เหตุผล" ด้วยหรือไม่? ตัวอย่างเช่นหากคำขอทั้งหมดส่งคืนเราขอให้อยู่ในระดับซุปเปอร์คลาสและใช้คลาสย่อยซึ่งเป็น POJO จริงจาก "เนื้อหา" ได้หรือไม่
Nima G

14

การแก้ปัญหาของ @ BrianRoach เป็นวิธีการแก้ปัญหาที่ถูกต้อง มันเป็นที่น่าสังเกตว่าในกรณีพิเศษที่คุณมีวัตถุที่กำหนดเองที่ซ้อนกันว่าทั้งความต้องการที่กำหนดเองTypeAdapterคุณจะต้องลงทะเบียนTypeAdapterกับตัวอย่างใหม่ของ GSONมิฉะนั้นสองTypeAdapterจะไม่ถูกเรียกว่า นี่เป็นเพราะเรากำลังสร้างGsonอินสแตนซ์ใหม่ภายใน deserializer ที่กำหนดเองของเรา

ตัวอย่างเช่นหากคุณมี json ต่อไปนี้:

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

และคุณต้องการให้ JSON นี้ถูกแมปกับวัตถุต่อไปนี้:

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

class SubContent
{
    public String baz;
}

คุณจะต้องลงทะเบียน'sSubContent TypeAdapterเพื่อให้มีประสิทธิภาพมากขึ้นคุณสามารถทำสิ่งต่อไปนี้:

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

    }
}

จากนั้นสร้างมันดังนี้:

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

สิ่งนี้สามารถใช้สำหรับกรณี "เนื้อหา" ที่ซ้อนกันได้อย่างง่ายดายเช่นกันโดยส่งผ่านอินสแตนซ์ใหม่ที่MyDeserializerมีค่า null


1
"Type" มาจากแพ็คเกจอะไร มีเป็นล้านแพ็คเกจที่มีคลาส "Type" ขอบคุณ.
Kyle Bridenstine

2
@ Mr.Tea มันจะjava.lang.reflect.Type
aidan

1
คลาส SubContentDeserializer อยู่ที่ไหน @KMarlow
LogronJ

10

ช้าไปหน่อย แต่หวังว่านี่จะช่วยใครบางคนได้

เพียงสร้าง TypeAdapterFactory ต่อไปนี้

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

และเพิ่มลงในตัวสร้าง GSON ของคุณ:

.registerTypeAdapterFactory(new ItemTypeAdapterFactory());

หรือ

 yourGsonBuilder.registerTypeAdapterFactory(new ItemTypeAdapterFactory());

นี่คือสิ่งที่ฉันมอง เนื่องจากฉันมีโหนด "data" หลายประเภทและฉันไม่สามารถเพิ่ม TypeAdapter ให้กับแต่ละโหนดได้ ขอบคุณ!
Sergey Irisov

@SergeyIrisov ยินดีต้อนรับคุณ คุณสามารถโหวตคำตอบนี้เพื่อให้สูงขึ้น :)
Matin Petrulak

วิธีการส่งหลายjsonElement?. เหมือนฉันมีcontent, content1ฯลฯ
Sathish Kumar J

ฉันคิดว่านักพัฒนาส่วนหลังของคุณควรเปลี่ยนโครงสร้างและไม่ผ่านเนื้อหา content1 ... ข้อดีของแนวทางนี้คืออะไร?
Matin Petrulak

ขอบคุณ! นี่คือคำตอบที่สมบูรณ์แบบ @Marin Petrulak: ข้อดีคือแบบฟอร์มนี้เป็นหลักฐานยืนยันการเปลี่ยนแปลงในอนาคต "content" คือเนื้อหาตอบกลับ ในอนาคตอาจมีช่องใหม่เช่น "version", "lastUpdated", "sessionToken" เป็นต้น หากคุณไม่ได้รวมเนื้อหาการตอบกลับไว้ล่วงหน้าคุณจะพบกับวิธีแก้ปัญหามากมายในโค้ดของคุณเพื่อปรับให้เข้ากับโครงสร้างใหม่
muetzenflo

7

มีปัญหาเดียวกันเมื่อสองสามวันก่อน ฉันแก้ปัญหานี้โดยใช้คลาส wrapper การตอบสนองและหม้อแปลง RxJava ซึ่งฉันคิดว่าเป็นวิธีแก้ปัญหาที่ยืดหยุ่นได้ดี:

เครื่องห่อ:

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

ข้อยกเว้นที่กำหนดเองในการโยนเมื่อสถานะไม่ตกลง:

public class ApiException extends RuntimeException {
    private final String reason;

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

    public String getReason() {
        return apiError;
    }
}

หม้อแปลง 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;
            });
}

ตัวอย่างการใช้งาน:

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

หัวข้อของฉัน: Retrofit 2 RxJava - Gson - deserialization "Global" เปลี่ยนประเภทการตอบกลับ


MyPojo มีลักษณะอย่างไร?
IgorGanapolsky

1
@IgorGanapolsky MyPojo สามารถมองได้ตามที่คุณต้องการ ควรตรงกับข้อมูลเนื้อหาของคุณที่ดึงมาจากเซิร์ฟเวอร์ โครงสร้างของคลาสนี้ควรปรับเป็นตัวแปลงอนุกรมของคุณ (Gson, Jackson ฯลฯ )
rafakob

@rafakob คุณช่วยฉันแก้ปัญหาด้วยได้ไหม มีช่วงเวลาที่ยากลำบากในการพยายามหาฟิลด์ใน json ที่ซ้อนกันของฉันด้วยวิธีที่ง่ายที่สุดเท่าที่จะทำได้ นี่คือคำถามของฉัน: stackoverflow.com/questions/56501897/…

6

การสานต่อความคิดของ Brian เนื่องจากเรามักจะมีทรัพยากร REST จำนวนมากซึ่งแต่ละอันมีรูทของตัวเองมันอาจมีประโยชน์ในการสรุป deserialization:

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

    }
}

จากนั้นในการแยกวิเคราะห์น้ำหนักบรรทุกตัวอย่างจากด้านบนเราสามารถลงทะเบียน GSON deserializer:

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

3

ทางออกที่ดีกว่านี้อาจเป็นได้ ..

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

จากนั้นกำหนดบริการของคุณเช่นนี้ ..

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

3

ตามคำตอบของ @Brian Roach และ @rafakob ฉันทำสิ่งนี้ด้วยวิธีต่อไปนี้

การตอบสนอง Json จากเซิร์ฟเวอร์

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

คลาสตัวจัดการข้อมูลทั่วไป

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 ที่กำหนดเอง

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

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

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

2

นี่เป็นวิธีแก้ปัญหาเดียวกับ @AYarulin แต่สมมติว่าชื่อคลาสเป็นชื่อคีย์ JSON ด้วยวิธีนี้คุณจะต้องส่งชื่อคลาสเท่านั้น

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

    }
}

จากนั้นในการแยกวิเคราะห์ข้อมูลตัวอย่างจากด้านบนเราสามารถลงทะเบียน GSON deserializer นี่เป็นปัญหาเนื่องจากคีย์เป็นแบบคำนึงถึงขนาดตัวพิมพ์ดังนั้นตัวพิมพ์เล็กและใหญ่ของชื่อคลาสจะต้องตรงกับตัวพิมพ์ใหญ่ของคีย์ JSON

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

2

นี่คือเวอร์ชัน Kotlin จากคำตอบของ Brian Roach และ 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)
    }
}

1

ในกรณีของฉันคีย์ "เนื้อหา" จะเปลี่ยนไปสำหรับการตอบกลับแต่ละครั้ง ตัวอย่าง:

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

ในกรณีเช่นนี้ฉันใช้วิธีแก้ปัญหาที่คล้ายกันตามรายการข้างต้น แต่ต้องปรับแต่ง ท่านสามารถเข้าดูเค้านี่ มีขนาดใหญ่เกินไปเล็กน้อยที่จะโพสต์ที่นี่บน SOF

@InnerKey("content")มีการใช้คำอธิบายประกอบและส่วนที่เหลือของรหัสเพื่ออำนวยความสะดวกในการใช้งานกับ Gson


คุณช่วยตอบคำถามของฉันด้วย มีเวลาในการรับฟิลด์จาก json ที่ซ้อนกันด้วยวิธีที่ง่ายที่สุด: stackoverflow.com/questions/56501897/…


0

อีกวิธีง่ายๆ:

JsonObject parsed = (JsonObject) new JsonParser().parse(jsonString);
Content content = gson.fromJson(parsed.get("content"), Content.class);
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.