Nhận đối tượng JSON lồng nhau với GSON bằng cách sử dụng trang bị thêm


111

Tôi đang sử dụng một API từ ứng dụng Android của mình và tất cả các phản hồi JSON đều như thế này:

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

Vấn đề là tất cả các POJO tôi có một status, reasonlĩnh vực, và bên trong contentlĩnh vực là POJO thực tôi muốn.

Có cách nào để tạo bộ chuyển đổi tùy chỉnh của Gson để trích xuất luôn contenttrường, vì vậy việc trang bị thêm trả về POJO đã hết hạn không?



Tôi đọc tài liệu nhưng tôi không thấy làm thế nào để làm điều đó ... :( Tôi không nhận ra làm thế nào để chương trình mã để giải quyết vấn đề của tôi
mikelar

Tôi tò mò tại sao bạn không chỉ định dạng lớp POJO của mình để xử lý các kết quả trạng thái đó.
jj.

Câu trả lời:


168

Bạn sẽ viết một trình giải mã tùy chỉnh trả về đối tượng được nhúng.

Giả sử JSON của bạn là:

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

Sau đó, bạn sẽ có một ContentPOJO:

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

Sau đó, bạn viết một 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);

    }
}

Bây giờ nếu bạn xây dựng một Gsonvới GsonBuildervà đăng ký bộ giải mã:

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

Bạn có thể giải mã JSON của mình thẳng tới Content:

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

Chỉnh sửa để thêm từ nhận xét:

Nếu bạn có các loại thư khác nhau nhưng tất cả đều có trường "nội dung", bạn có thể đặt Deserializer chung bằng cách:

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

    }
}

Bạn chỉ cần đăng ký một phiên bản cho từng loại của mình:

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

Khi bạn gọi .fromJson(), kiểu này được đưa vào bộ khử không khí, vì vậy nó sẽ hoạt động với tất cả các kiểu của bạn.

Và cuối cùng khi tạo một phiên bản Retrofit:

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

1
ồ điều đó thật tuyệt! cảm ơn! : D Có cách nào để tổng quát hóa giải pháp để tôi không phải tạo một JsonDeserializer cho mỗi loại phản hồi không?
mikelar

1
Thật đáng kinh ngạc! Một điều cần thay đổi: Gson gson = new GsonBuilder (). Create (); Thay vì Gson gson = new GsonBuilder (). Build (); Có hai trường hợp của điều này.
Nelson Osacky

7
@feresr bạn có thể gọi setConverter(new GsonConverter(gson))trong RestAdapter.Builderlớp học của Retrofit
akhy

2
@BrianRoach cảm ơn, câu trả lời hay .. tôi có nên đăng ký Person.classList<Person>.class/ Person[].classvới Deserializer riêng biệt không?
akhy

2
Bất kỳ khả năng để có được "trạng thái" và "lý do" quá? Ví dụ: nếu tất cả các yêu cầu trả về chúng, chúng ta có thể đặt chúng trong một lớp siêu và sử dụng các lớp con là POJO thực sự từ "nội dung" không?
Nima G

14

Giải pháp của @ BrianRoach là giải pháp chính xác. Cần lưu ý rằng trong trường hợp đặc biệt khi bạn có các đối tượng tùy chỉnh lồng nhau mà cả hai đều cần tùy chỉnh TypeAdapter, bạn phải đăng ký phiên TypeAdapterbản mới của GSON , nếu không đối tượng thứ hai TypeAdaptersẽ không bao giờ được gọi. Điều này là do chúng tôi đang tạo một phiên bản mới Gsonbên trong trình giải mã tùy chỉnh của chúng tôi.

Ví dụ: nếu bạn có json sau:

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

Và bạn muốn JSON này được ánh xạ tới các đối tượng sau:

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

class SubContent
{
    public String baz;
}

Bạn sẽ cần phải đăng ký SubContent's TypeAdapter. Để mạnh mẽ hơn, bạn có thể làm như sau:

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

    }
}

và sau đó tạo nó như vậy:

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

Điều này cũng có thể dễ dàng được sử dụng cho trường hợp "nội dung" lồng nhau bằng cách chỉ cần chuyển vào một trường hợp mới MyDeserializercó giá trị null.


1
"Loại" từ gói nào? Có một triệu gói chứa lớp "Loại". Cảm ơn bạn.
Kyle Bridenstine

2
@ Mr.Tea Nó sẽ làjava.lang.reflect.Type
aidan

1
Lớp SubContentDeserializer ở đâu? @KMarlow
LogronJ

10

Hơi muộn nhưng hy vọng điều này sẽ giúp ích cho ai đó.

Chỉ cần tạo TypeAdapterFactory sau đây.

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

và thêm nó vào trình tạo GSON của bạn:

.registerTypeAdapterFactory(new ItemTypeAdapterFactory());

hoặc là

 yourGsonBuilder.registerTypeAdapterFactory(new ItemTypeAdapterFactory());

Đây chính xác là những gì tôi nhìn. Bởi vì tôi có rất nhiều loại được bao bọc bằng nút "dữ liệu" và tôi không thể thêm TypeAdapter vào từng loại đó. Cảm ơn!
Sergey Irisov

@SergeyIrisov bạn được chào đón. Bạn có thể bỏ phiếu lên câu trả lời này để nó có được cao hơn :)
Matin Petrulak

Làm thế nào để vượt qua nhiều lần jsonElement?. như tôi có content, content1v.v.
Sathish Kumar J

Tôi nghĩ các nhà phát triển back-end của bạn nên thay đổi cấu trúc và không chuyển nội dung, nội dung1 ... Ưu điểm của cách làm này là gì?
Matin Petrulak

Cảm ơn bạn! Đây là câu trả lời hoàn hảo. @Marin Petrulak: Ưu điểm là biểu mẫu này là bằng chứng cho những thay đổi trong tương lai. "content" là nội dung phản hồi. Trong tương lai, chúng có thể xuất hiện các trường mới như "phiên bản", "lastUpdated", "sessionToken", v.v. Nếu bạn không gói nội dung phản hồi của mình trước đó, bạn sẽ gặp phải nhiều cách giải quyết trong mã của mình để thích ứng với cấu trúc mới.
muetzenflo

7

Đã gặp vấn đề tương tự vài ngày trước. Tôi đã giải quyết vấn đề này bằng cách sử dụng lớp trình bao bọc phản hồi và biến áp RxJava, mà tôi nghĩ là giải pháp khá linh hoạt:

Vỏ bánh:

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

Tùy chỉnh ngoại lệ để ném, khi trạng thái không ổn:

public class ApiException extends RuntimeException {
    private final String reason;

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

    public String getReason() {
        return apiError;
    }
}

Máy biến áp 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;
            });
}

Ví dụ sử dụng:

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

Chủ đề của tôi: Trang bị thêm 2 RxJava - Gson - deserialization "Toàn cầu", loại phản hồi thay đổi


MyPojo trông như thế nào?
IgorGanapolsky

1
@IgorGanapolsky MyPojo có thể trông theo cách bạn muốn. Nó phải khớp với dữ liệu nội dung của bạn được lấy từ máy chủ. Cấu trúc của lớp này nên được điều chỉnh theo bộ chuyển đổi tuần tự hóa của bạn (Gson, Jackson, v.v.).
rafakob

@rafakob, bạn cũng có thể giúp tôi giải quyết vấn đề của tôi được không? Gặp khó khăn khi cố gắng lấy một trường trong json lồng nhau của tôi theo cách đơn giản nhất có thể. đây là câu hỏi của tôi: stackoverflow.com/questions/56501897/…

6

Tiếp tục ý tưởng của Brian, vì chúng ta hầu như luôn có nhiều tài nguyên REST, mỗi tài nguyên đều có gốc riêng, nên có thể hữu ích khi khái quát hóa quá trình giải hóa:

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

    }
}

Sau đó, để phân tích cú pháp trọng tải mẫu từ phía trên, chúng ta có thể đăng ký bộ giải mã GSON:

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

3

Một giải pháp tốt hơn có thể là ..

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

Sau đó, xác định dịch vụ của bạn như thế này ..

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

3

Theo câu trả lời của @Brian Roach và @rafakob, tôi đã thực hiện việc này theo cách sau

Phản hồi Json từ máy chủ

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

Lớp xử lý dữ liệu chung

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

    @SerializedName("code")
    public int code;

    @SerializedName("message")
    public String reason;

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

Bộ nối tiếp tùy chỉnh

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

      }
}

Đối tượng Gson

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

Cuộc gọi 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

Đây là giải pháp tương tự như @AYarulin nhưng giả sử tên lớp là tên khóa JSON. Bằng cách này, bạn chỉ cần nhập tên Lớp.

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

    }
}

Sau đó, để phân tích cú pháp trọng tải mẫu từ phía trên, chúng ta có thể đăng ký bộ giải mã GSON. Điều này có vấn đề vì Khóa phân biệt chữ hoa chữ thường, vì vậy trường hợp của tên lớp phải khớp với trường hợp của khóa JSON.

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

2

Đây là phiên bản Kotlin dựa trên câu trả lời của Brian Roach và 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

Trong trường hợp của tôi, khóa "nội dung" sẽ thay đổi cho mỗi phản hồi. Thí dụ:

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

Trong những trường hợp như vậy, tôi đã sử dụng một giải pháp tương tự như được liệt kê ở trên nhưng phải tinh chỉnh nó. Bạn có thể xem ý chính ở đây . Nó hơi quá lớn để đăng nó ở đây trên SOF.

Chú thích @InnerKey("content")được sử dụng và phần còn lại của mã là để hỗ trợ việc sử dụng nó với Gson.


Bạn cũng có thể giúp tôi với câu hỏi của tôi. Có một thời gian HRD nhận một trường từ một json lồng nhau trong cách đơn giản nhất có thể: stackoverflow.com/questions/56501897/...


0

Một giải pháp đơn giản khác:

JsonObject parsed = (JsonObject) new JsonParser().parse(jsonString);
Content content = gson.fromJson(parsed.get("content"), Content.class);
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.