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


111

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

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

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

항상 content필드 를 추출하기 위해 Gson의 사용자 지정 변환기를 만드는 방법이 있습니까? 그러면 개조가 적절한 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);

    }
}

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

1
우와 그거 굉장 하군! 감사! : D 각 응답 유형에 대해 하나의 JsonDeserializer를 만들 필요가 없도록 솔루션을 일반화하는 방법이 있습니까?
mikelar 2014

1
이것은 놀랍다! 변경해야 할 사항 : Gson gson = new GsonBuilder (). create (); Gson 대신 gson = new GsonBuilder (). build (); 두 가지 사례가 있습니다.
Nelson Osacky 2014

7
@feresr 당신은 setConverter(new GsonConverter(gson))Retrofit의 RestAdapter.Builder수업 에서 전화 할 수 있습니다
akhy

2
@BrianRoach 덕분에, 좋은 대답은 ... 내가 등록해야 Person.class하고 List<Person>.class/ Person[].class분리 디시리얼라이저로?
akhy 2014-06-04

2
"상태"와 "이유"도 알 수 있습니까? 예를 들어 모든 요청이이를 반환하는 경우이를 수퍼 클래스에 포함하고 "content"의 실제 POJO 인 하위 클래스를 사용할 수 있습니까?
Nima G

14

@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"케이스에 대해 쉽게 사용할 수 있습니다 .


1
"유형"의 패키지는 무엇입니까? "Type"클래스를 포함하는 백만 개의 패키지가 있습니다. 감사합니다.
Kyle Bridenstine 2015 년

2
@ Mr.Tea It 'll bejava.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());

이것이 바로 내가 보는 것입니다. 많은 유형이 "데이터"노드로 래핑되어 있고 각각에 TypeAdapter를 추가 할 수 없기 때문입니다. 감사!
Sergey Irisov 2017-06-26

@SergeyIrisov 당신은 환영합니다. 가 : 높은 얻을 수 있도록 최대이 답변을 투표 할 수
도착하게 Petrulak에게

여러 통과하는 방법 jsonElement?. 난이처럼 content, content1
사티시 쿠마르 J

백엔드 개발자는 구조를 변경하고 콘텐츠, content1을 전달하지 않아야한다고 생각합니다.이 접근 방식의 장점은 무엇입니까?
Matin Petrulak

감사합니다! 이것이 완벽한 답입니다. @Marin Petrulak : 장점은이 양식이 변화에 대한 미래 보장형이라는 것입니다. "content"는 응답 내용입니다. 미래에는 "version", "lastUpdated", "sessionToken"등과 같은 새로운 필드가 올 수 있습니다. 미리 응답 콘텐츠를 래핑하지 않은 경우 코드에서 여러 가지 해결 방법을 실행하여 새 구조에 적응할 수 있습니다.
muetzenflo

7

며칠 전에 같은 문제가있었습니다. 응답 래퍼 클래스와 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- "글로벌"역 직렬화, 응답 유형 변경


MyPojo는 어떻게 생겼습니까?
IgorGanapolsky

1
@IgorGanapolsky MyPojo는 원하는대로 볼 수 있습니다. 서버에서 검색 한 콘텐츠 데이터와 일치해야합니다. 이 클래스의 구조는 직렬화 변환기 (Gson, Jackson 등)에 맞게 조정해야합니다.
rafakob

@rafakob 내 문제도 도와 줄 수 있습니까? 가능한 가장 간단한 방법으로 중첩 된 json에서 필드를 가져 오는 데 어려움을 겪으십시오. 여기 내 질문이 있습니다 : stackoverflow.com/questions/56501897/…

6

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

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

커스텀 시리얼 라이저

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

다음은 Brian Roach와 AYarulin의 답변을 기반으로 한 Kotlin 버전입니다.

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에서 필드를 가져


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.