2016-05-14 2 views
9

Я использую Retrofit 2 и Gson, и у меня возникают проблемы с десериализацией ответов от моего API. Вот мой сценарий:Использование Gson и Retrofit 2 для десериализации сложных ответов 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 в общей обертке с важной частью внутри e data раздел.
  2. Отдачи API объектов в формате, который непосредственно не соответствует полям на модели (например, значение берется из id_to_age потребности быть отображенным на age полей на модели)
  3. 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(); 

} 

Это ранее вопрос, который я отправил обсуждает свою первую попытку, но она не учитывает некоторые из подводных камней упомянутый выше: Using Retrofit and RxJava, how do I deserialize JSON when it doesn't map directly to a model object?

+0

Вы говорите, что "мой" API. Если у вас есть доступ к серверу, вы должны сделать сериализацию возраста и имени лучше на стороне сервера. – iagreen

+0

У меня нет доступа. Под «моим API» я имею в виду API, с которым я работаю. – user2393462435

+0

Почему вы не создаете простые старые объекты Java, которые представляют ваши ответы JSON, а затем сопоставляют их со своим классом Employee? –

ответ

7

EDIT: Соответствующее обновление: создание пользовательского преобразователя завод ДЕЛАЕТ работа - ключ к избежанию бесконечный цикл через ApiResponseConverterFactory-х является вызов дооснащения-х nextResponseBodyConverter, который позволяет указать фабрику для того чтобы пропустить. Ключом является то, что это будет Converter.Factory для регистрации с помощью Retrofit, а не TypeAdapterFactory для Gson. Это было бы действительно предпочтительнее, поскольку оно предотвращает двойную десериализацию ResponseBody (нет необходимости десериализовать тело, а затем повторно упаковать его снова как другой ответ).

See the gist here for an implementation example.

ОРИГИНАЛЬНЫЙ ОТВЕТ:

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 должны следовать the adapter factory construct I wrote in the previous question. Теперь все поля ApiResponse должны потребляться перехватчиком, и каждый сделанный вами досрочный вызов должен возвращать только интересующий вас контент JSON.

+0

Отличная идея! Полностью имеет смысл, и это может быть даже полезным подходом для других прихожей API. Еще раз спасибо за вашу помощь по обоим вопросам. – user2393462435

+0

Нет проблем. Дайте мне знать, если есть какие-либо проблемы с этим подходом, на этот раз должно быть хорошо! – ekchang

5

Я бы предложил использовать 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, так что требует немного больше работы, чтобы разобрать его.

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()); 
Смежные вопросы