Использование Gson и Retrofit 2 для десериализации сложных ответов API
я использую Retrofit 2
и Gson
и у меня возникли проблемы с десериализацией ответов из моего API. Вот мой сценарий:
у меня есть объект модели с именем Employee
, который имеет три поля: id
, name
, age
.
у меня есть API, который возвращает единственное Employee
объект вроде этого:
{
"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
}
},
]
}
есть три основные вещи, чтобы рассмотреть здесь:
- API-интерфейс ответы возвращаются в общей оболочке, с важной частью внутри
3 ответов
EDIT: соответствующее обновление: создание фабрики пользовательских конвертеров действительно работает-ключ к избежанию бесконечного цикла через ApiResponseConverterFactory
- это позвонить по модернизации nextResponseBodyConverter
, который позволяет указать фабрику, чтобы пропустить. Ключ это будет Converter.Factory
зарегистрироваться в Retrofit, а не TypeAdapterFactory
для Gson. Это было бы предпочтительнее, поскольку это предотвращает двойную десериализацию ResponseBody (нет необходимости десериализовать тело, а затем снова упаковать его как другой ответ.)
см. gist здесь для примера реализации.
ОРИГИНАЛЬНЫЙ ОТВЕТ:
на ApiResponseAdapterFactory
подход не работает, если вы не готовы обернуть все свои интерфейсы обслуживания с ApiResponse<T>
. Однако есть и другой вариант: перехватчики 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 и дооснащение:
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new ApiResponseInterceptor())
.build();
Retrofit retrofit = new Retrofit.Builder()
.client(client)
.build();
и Employee
и EmployeeResponse
должен следовать конструкция фабрики адаптера, которую я написал в предыдущем вопросе. Сейчас все ApiResponse
поля должны потребляться перехватчиком, и каждый модифицированный вызов, который вы делаете, должен возвращать только содержимое 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 whitch переводится на карту на Java, поэтому для его анализа требуется немного больше работы.
просто создайте следующий 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 builder:
.registerTypeAdapterFactory(new ItemTypeAdapterFactory());
или
yourGsonBuilder.registerTypeAdapterFactory(new ItemTypeAdapterFactory());