web-dev-qa-db-ja.com

GsonとRetrofit 2を使用して複雑なAPI応答をデシリアライズする

Retrofit 2とGsonを使用していますが、APIからの応答のデシリアライズに問題があります。これが私のシナリオです:

Employeeidnameの3つのフィールドがあるageという名前のモデルオブジェクトがあります。

次のような単一のEmployeeオブジェクトを返すAPIがあります。

{
    "status": "success",
    "code": 200,
    "data": {
        "id": "123",
        "id_to_name": {
            "123" : "John Doe"
        },
        "id_to_age": {
            "123" : 30
        }
    }
}

そして、次のようなEmployeeオブジェクトのリスト:

{
    "status": "success",
    "code": 200,
    "data": [
        {
            "id": "123",
            "id_to_name": {
                "123" : "John Doe"
            },
            "id_to_age": {
                "123" : 30
            }
        },
        {
            "id": "456",
            "id_to_name": {
                "456" : "Jane Smith"
            },
            "id_to_age": {
                "456" : 35
            }
        },
    ]
}

ここで考慮すべき主なものは3つあります。

  1. API応答は、dataフィールド内の重要な部分とともに、汎用ラッパーで返されます。
  2. APIは、モデルのフィールドに直接対応しない形式でオブジェクトを返します(たとえば、id_to_ageから取得した値は、モデルのageフィールドにマップする必要があります)
  3. API応答のdataフィールドは、単一のオブジェクトまたはオブジェクトのリストにすることができます。

これらの3つのケースをエレガントに処理するように、Gsonを使用して逆シリアル化を実装するにはどうすればよいですか?

理想的には、TypeAdapterのパフォーマンスペナルティを支払う代わりに、TypeAdapterFactoryまたはJsonDeserializerを使用してこれを完全に実行することをお勧めします。最終的には、このインターフェースを満たすようなEmployeeまたはList<Employee>のインスタンスを作成したいと思います。

public interface EmployeeService {

    @GET("/v1/employees/{employee_id}")
    Observable<Employee> getEmployee(@Path("employee_id") String employeeId);

    @GET("/v1/employees")
    Observable<List<Employee>> getEmployees();

}

私が投稿したこの以前の質問は、これに対する私の最初の試みについて説明していますが、上記のいくつかの落とし穴を考慮することはできません: RetrofitとRxJavaを使用して、JSONに直接マッピングしない場合にJSONを逆シリアル化する方法モデルオブジェクト?

15
user2393462435

EDIT:関連する更新:カスタムコンバータファクトリの作成は機能します-ApiResponseConverterFactoryを介した無限ループを回避するための鍵は、RetrofitのnextResponseBodyConverterを使用すると、スキップするファクトリを指定できます。重要なのは、GsonのTypeAdapterFactoryではなく、Retrofitに登録するための_Converter.Factory_であることです。これは、ResponseBodyの二重逆シリアル化を防止するため、実際には望ましいでしょう(ボディを逆シリアル化してから、別の応答として再度パッケージ化する必要はありません)。

実装例については、ここの要旨を参照してください。

元の回答:

すべてのサービスインターフェイスを_ApiResponse<T>_でラップするつもりがない限り、ApiResponseAdapterFactoryアプローチは機能しません。ただし、別のオプションがあります:OkHttpインターセプター。

これが私たちの戦略です:

  • 特定のレトロフィット構成では、Responseをインターセプトするアプリケーションインターセプターを登録します
  • Response#body()ApiResponseとして逆シリアル化され、新しいResponseが返されます。ここで、ResponseBodyは必要なコンテンツだけです。

したがって、ApiResponseは次のようになります。

_public class ApiResponse {
  String status;
  int code;
  JsonObject data;
}
_

ApiResponseInterceptor:

_public class ApiResponseInterceptor implements Interceptor {
  public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
  public static final Gson GSON = new Gson();

  @Override
  public Response intercept(Chain chain) throws IOException {
    Request request = chain.request();
    Response response = chain.proceed(request);
    final ResponseBody body = response.body();
    ApiResponse apiResponse = GSON.fromJson(body.string(), ApiResponse.class);
    body.close();

    // TODO any logic regarding ApiResponse#status or #code you need to do 

    final Response.Builder newResponse = response.newBuilder()
        .body(ResponseBody.create(JSON, apiResponse.data.toString()));
    return newResponse.build();
  }
}
_

OkHttpとRetrofitを構成します。

_OkHttpClient client = new OkHttpClient.Builder()
        .addInterceptor(new ApiResponseInterceptor())
        .build();
Retrofit retrofit = new Retrofit.Builder()
        .client(client)
        .build();
_

そしてEmployeeEmployeeResponseが続くはずです 前の質問で書いたアダプタファクトリの構成 。これで、すべてのApiResponseフィールドがインターセプターによって使用され、Retrofitを呼び出すたびに、関心のあるJSONコンテンツのみが返されるようになります。

7
ekchang

JsonDeserializerを使用することをお勧めします。応答にはネストのレベルがそれほど多くないため、パフォーマンスに大きな影響はありません。

クラスは次のようになります。

一般的な応答のためにサービスインターフェイスを調整する必要があります。

interface EmployeeService {

    @GET("/v1/employees/{employee_id}")
    Observable<DataResponse<Employee>> getEmployee(@Path("employee_id") String employeeId);

    @GET("/v1/employees")
    Observable<DataResponse<List<Employee>>> getEmployees();

}

これは一般的なデータ応答です:

class DataResponse<T> {

    @SerializedName("data") private T data;

    public T getData() {
        return data;
    }
}

従業員モデル:

class Employee {

    final String id;
    final String name;
    final int age;

    Employee(String id, String name, int age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

}

従業員のデシリアライザ:

class EmployeeDeserializer implements JsonDeserializer<Employee> {

    @Override
    public Employee deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
            throws JsonParseException {

        JsonObject employeeObject = json.getAsJsonObject();
        String id = employeeObject.get("id").getAsString();
        String name = employeeObject.getAsJsonObject("id_to_name").entrySet().iterator().next().getValue().getAsString();
        int age = employeeObject.getAsJsonObject("id_to_age").entrySet().iterator().next().getValue().getAsInt();

        return new Employee(id, name, age);
    }
}

レスポンスの問題は、nameageがJSONオブジェクト内に含まれていて、JavaのMapに変換されるため、もう少し作業が必要です。解析します。

6
Dejan Tošić

次の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("data")) {
                    jsonElement = jsonObject.get("data");
                }
            }

            return delegate.fromJsonTree(jsonElement);
        }
    }.nullSafe();
}

}

それをGSONビルダーに追加します。

.registerTypeAdapterFactory(new ItemTypeAdapterFactory());

または

 yourGsonBuilder.registerTypeAdapterFactory(new ItemTypeAdapterFactory());
3
Matin Petrulak