レトロフィットを使用してGSONでネストされたJSONオブジェクトを取得する


111

私はAndroidアプリからAPIを使用しており、すべてのJSON応答は次のようになります。

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

問題は、すべてのPOJOにstatusreasonフィールドがあり、フィールド内にcontent私が欲しい本物のPOJOがあることです。

常にcontentフィールドを抽出するGsonのカスタムコンバーターを作成する方法はありますか?そのため、レトロフィットは適切なPOJOを返しますか?



文書を読んでも方法がわかりません... :(問題を解決するためのコードのプログラミング方法がわかりません
mikelar

これらのステータス結果を処理するためにPOJOクラスをフォーマットしないのはなぜでしょうか。
jj。

回答:


168

埋め込みオブジェクトを返すカスタムデシリアライザを記述します。

JSONが次のとおりだとします。

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

その後、ContentPOJOを取得します。

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

次に、デシリアライザを記述します。

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

    }
}

次に、を作成しGsonGsonBuilderデシリアライザを登録すると、

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

JSONを次のように直接デシリアライズできますContent

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

コメントから追加する編集:

さまざまなタイプのメッセージがあり、それらすべてに「コンテンツ」フィールドがある場合、次のようにしてデシリアライザをジェネリックにすることができます。

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()型を呼び出すと、デシリアライザーに持ち込まれるため、すべての型で機能するはずです。

そして最後にRetrofitインスタンスを作成するとき:

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

1
うわー、それは素晴らしいです!ありがとう!:Dソリューションを一般化する方法はあるので、応答のタイプごとに1つのJsonDeserializerを作成する必要はありませんか?
ミケラー

1
これは素晴らしいです!変更すること:Gson gson = new GsonBuilder()。create(); Gsonの代わりにgson = new GsonBuilder()。build(); これには2つの例があります。
Nelson Osacky 2014

7
@feresr setConverter(new GsonConverter(gson))はレトロフィットのRestAdapter.Builderクラスで呼び出すことができます
akhy

2
@BrianRoachありがとう、いい答え.. デシリアライザを分離して/ 登録Person.classしますか?List<Person>.classPerson[].class
akhy

2
「ステータス」と「理由」も取得できる可能性はありますか?たとえば、すべてのリクエストがそれらを返す場合、それらをスーパークラスに入れて、「コンテンツ」からの実際のPOJOであるサブクラスを使用できますか?
Nima G

14

@BrianRoachのソリューションは正しいソリューションです。どちらもcustomを必要とするネストされたカスタムオブジェクトがある特殊なケースではTypeAdapter、をGSONTypeAdapter新しいインスタンスに登録する必要TypeAdapterがあります。それ以外の場合、2番目は呼び出されません。これはGson、カスタムデシリアライザー内に新しいインスタンスを作成しているためです。

たとえば、次の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();

これは、ネストされた「コンテンツ」の場合にもMyDeserializer、null値を持つの新しいインスタンスを渡すだけで簡単に使用できます。


1
「タイプ」のパッケージは何ですか?クラス「Type」を含むパッケージは100万あります。ありがとうございました。
カイルブリデンスティン2015年

2
Mr.Tea @それはなるだろうjava.lang.reflect.Type
エイダン

1
SubContentDeserializerクラスはどこにありますか?@KMarlow
LogronJ 2018

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年

@SergeyIrisovどういたしまして。あなたはこの回答に賛成票を投じて、それを高くすることができます:)
Matin Petrulak 2017年

複数を渡す方法はjsonElement?私が持っているようにcontentcontent1など
サティッシュ・クマールJ

私はあなたのバックエンド開発者が構造を変更し、コンテンツcontent1を渡さないようにすべきだと思います...このアプローチの利点は何ですか?
Matin Petrulak 2017

ありがとうございました!これは完璧な答えです。@Marin Petrulak:利点は、このフォームが将来の変更に対応できることです。「コンテンツ」は応答コンテンツです。将来的には、「version」、「lastUpdated」、「sessionToken」などの新しいフィールドが追加される可能性があります。事前に応答コンテンツをラップしていなかった場合、新しい構造に適応するためにコードで一連の回避策に出くわします。
muetzenflo

7

数日前に同じ問題がありました。私はこれを、応答ラッパークラスとRxJavaトランスフォーマーを使用して解決しました。これは非常に柔軟なソリューションだと思います。

ラッパー:

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

ステータスがOKでない場合にスローするカスタム例外:

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 2016年

@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デシリアライザを登録できます。

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デシリアライザを登録できます。キーでは大文字と小文字が区別されるため、これには問題があり、クラス名の大文字と小文字は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からフィールドを取得HRDの時間を持っている: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.