Использование 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
            }
        },
    ]
}

есть три основные вещи, чтобы рассмотреть здесь:

  1. 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());