Retrofit 2とGsonを使用していますが、APIからの応答のデシリアライズに問題があります。これが私のシナリオです:
Employee
、id
、name
の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つあります。
data
フィールド内の重要な部分とともに、汎用ラッパーで返されます。id_to_age
から取得した値は、モデルのage
フィールドにマップする必要があります)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を逆シリアル化する方法モデルオブジェクト?
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();
_
そしてEmployee
とEmployeeResponse
が続くはずです 前の質問で書いたアダプタファクトリの構成 。これで、すべてのApiResponse
フィールドがインターセプターによって使用され、Retrofitを呼び出すたびに、関心のあるJSONコンテンツのみが返されるようになります。
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);
}
}
レスポンスの問題は、name
とage
がJSONオブジェクト内に含まれていて、JavaのMapに変換されるため、もう少し作業が必要です。解析します。
次の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());