Использование 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 возвращаются в универсальной оболочке с важной частью внутри поля
data
. - API возвращает объекты в формате, который не соответствует непосредственно полям модели (например, значение, взятое из
id_to_age
должно быть сопоставлено с полем age
модели) - Поле
data
в ответе API может быть единичным объектом или списком объектов.
Как реализовать десериализацию с помощью 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, когда он не отображается непосредственно на объект модели?
Ответы
Ответ 1
РЕДАКТИРОВАТЬ: Релевантное обновление: создание пользовательского конвертера factory Работает - ключом к предотвращению бесконечного цикла через ApiResponseConverterFactory
является вызов Retrofit nextResponseBodyConverter
, который позволяет вам указать factory, чтобы пропустить. Ключ это будет Converter.Factory
для регистрации с помощью Retrofit, а не TypeAdapterFactory
для Gson. Это было бы предпочтительнее, поскольку оно предотвращает двойную десериализацию ResponseBody (нет необходимости десериализовать тело, а затем повторно упаковать его снова как другой ответ).
Смотрите пример для примера реализации.
ОРИГИНАЛЬНЫЙ ОТВЕТ:
Подход 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
должны следовать конструктору адаптера factory, который я написал в предыдущем вопросе. Теперь все поля ApiResponse
должны потребляться перехватчиком, и каждый сделанный вами повторный доработок должен возвращать только интересующий вас контент JSON.
Ответ 2
Я бы предложил использовать 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 переводит на Map в Java, поэтому для его анализа требуется немного больше работы.
Ответ 3
Просто создайте следующий 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());