developer tip

개조를 사용하여 GSON으로 중첩 된 JSON 개체 가져 오기

optionbox 2020. 8. 13. 08:16
반응형

개조를 사용하여 GSON으로 중첩 된 JSON 개체 가져 오기


내 Android 앱에서 API를 사용하고 있으며 모든 JSON 응답은 다음과 같습니다.

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

문제는 모든 POJO에 status, reason필드 가 있고 필드 내부에 content내가 원하는 실제 POJO가 있다는 것입니다.

항상 content필드 를 추출하기 위해 Gson의 사용자 지정 변환기를 만드는 방법이 있습니까? 그러면 개조가 적절한 POJO를 반환합니까?


포함 된 개체를 반환하는 사용자 지정 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);

    }
}

이제 Gsonwith 를 생성 GsonBuilder하고 deserializer를 등록하면 :

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

JSON을 직접 역 직렬화 할 수 있습니다 Content.

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

댓글에서 추가하려면 수정 :

다른 유형의 메시지가 있지만 모두 "content"필드가있는 경우 다음을 수행하여 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();

@BrianRoach의 솔루션이 올바른 솔루션입니다. 둘 다 custom이 필요한 중첩 된 사용자 지정 개체가있는 특수한 경우 에는를 GSON새 인스턴스에TypeAdapter 등록해야합니다 . 그렇지 않으면 두 번째 개체 가 호출되지 않습니다. 이는 사용자 지정 deserializer 내에 인스턴스를 생성하기 때문 입니다.TypeAdapterTypeAdapterGson

예를 들어 다음과 같은 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;
}

당신은 등록 할 필요가있을 것이다 SubContent'들 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();

이것은 단순히 MyDeserializernull 값 가진 새로운 인스턴스를 전달함으로써 중첩 된 "content"케이스에 대해 쉽게 사용될 수 있습니다 .


조금 늦었지만 바라건대 이것은 누군가를 도울 것입니다.

다음 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());

Brian의 아이디어를 계속 이어 가면 거의 항상 자체 루트가있는 많은 REST 리소스가 있기 때문에 역 직렬화를 일반화하는 것이 유용 할 수 있습니다.

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

며칠 전에 같은 문제가 발생했습니다. 응답 래퍼 클래스와 RxJava 변환기를 사용하여이 문제를 해결했습니다. 이는 매우 유연한 솔루션이라고 생각합니다.

싸개:

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

상태가 OK가 아닌 경우 throw 할 사용자 지정 예외 :

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- "글로벌"역 직렬화, 응답 유형 변경


더 나은 해결책이 될 수 있습니다 ..

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

그런 다음 이와 같이 서비스를 정의하십시오.

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

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

커스텀 시리얼 라이저

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

이것은 @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);

    }
}

Then to parse sample payload from above, we can register GSON deserializer. This is problematic as the Key is case sensitive, so the case of the class name must match the case of the JSON key.

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

Here's a Kotlin version based on the answers by Brian Roach and 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)
    }
}

In my case, the "content" key would change for each response. Example:

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

In such cases I used a similar solution as listed above but had to tweak it. You can see the gist here. It's a little too large to post it here on SOF.

The annotation @InnerKey("content") is used and the rest of the code is to facilitate it's usage with Gson.


Don't forget @SerializedName and @Expose annotations for all Class members and Inner Class members that most deserialized from JSON by GSON.

Look at https://stackoverflow.com/a/40239512/1676736


Another simple solution:

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

참고URL : https://stackoverflow.com/questions/23070298/get-nested-json-object-with-gson-using-retrofit

반응형