【问题标题】:Using Gson and Retrofit 2 to deserialize complex API responses使用 Gson 和 Retrofit 2 反序列化复杂的 API 响应
【发布时间】:2016-05-14 21:08:39
【问题描述】:

我正在使用 Retrofit 2 和 Gson,但在反序列化来自我的 API 的响应时遇到了问题。这是我的场景:

我有一个名为 Employee 的模型对象,它具有三个字段:idnameage

我有一个返回单个 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
            }
        },
    ]
}

这里需要考虑三个主要事项:

  1. API 响应在通用包装器中返回,重要部分位于 data 字段中。
  2. API 以与模型上的字段不直接对应的格式返回对象(例如,取自 id_to_age 的值需要映射到模型上的 age 字段)
  3. API 响应中的data 字段可以是单个对象,也可以是对象列表。

如何使用Gson 实现反序列化,以便优雅地处理这三种情况?

理想情况下,我宁愿完全使用TypeAdapterTypeAdapterFactory 来完成此操作,而不是支付JsonDeserializer 的性能损失。最终,我想得到一个 EmployeeList<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?

【问题讨论】:

  • 你说“我的 API”。如果你可以访问后端,你应该在服务器端更好地进行年龄和名称的序列化。
  • 我没有访问权限。 “我的 API”是指我正在使用的 API。
  • 为什么不创建简单的旧 Java 对象来表示您的 JSON 响应,然后将它们映射到您的 Employee 类?
  • 这就是我为模型部分所做的(请参阅我帖子末尾的另一个链接),但我无法弄清楚如何做到这一点并处理通用包装器并处理事实上,包装器内的响应可以是对象或数组。

标签: android gson deserialization retrofit


【解决方案1】:

我建议使用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);
    }
}

响应的问题在于 nameage 包含在 JSON 对象中,该对象在 Java 中转换为 Map,因此需要更多的工作来解析它。

【讨论】:

  • 你在哪里调用你的解串器?
【解决方案2】:

编辑: 相关更新:创建自定义转换器工厂确实有效——避免通过 ApiResponseConverterFactory 的无限循环的关键是调用 Retrofit 的 nextResponseBodyConverter,它允许您指定工厂跳过。关键是这将是一个 Converter.Factory 来注册 Retrofit,而不是 TypeAdapterFactory 用于 Gson。这实际上更可取,因为它可以防止 ResponseBody 的双重反序列化(无需反序列化主体然后将其重新打包为另一个响应)。

See the gist here for an implementation example.

原始答案:

除非您愿意用ApiResponse&lt;T&gt; 包装所有服务接口,否则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 和改造:

OkHttpClient client = new OkHttpClient.Builder()
        .addInterceptor(new ApiResponseInterceptor())
        .build();
Retrofit retrofit = new Retrofit.Builder()
        .client(client)
        .build();

EmployeeEmployeeResponse 应该跟在 the adapter factory construct I wrote in the previous question 后面。现在所有的 ApiResponse 字段都应该被拦截器使用,并且您进行的每个 Retrofit 调用都应该只返回您感兴趣的 JSON 内容。

【讨论】:

  • 好主意!完全有道理,它甚至可能是其他 API 怪癖的有用方法。再次感谢您对这两个问题的帮助。
  • 没问题。如果这种方法有任何问题,请告诉我,这次应该很好!
【解决方案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());

【讨论】:

    【解决方案4】:

    我必须说我没有考虑过使用Interceptors 来处理这样的事情,但这是一种有趣的方法。当我需要对后端包装器响应进行建模时,我通常会这样做:

    如果你从后端得到这样的东西:

    {
      "success": "success", // Let's say you may get "error", "unauthorized", etc.
      "payload": [...] // Let's say that you may either get a json object or an array.
    }
    

    然后你可以声明一个反序列化器:

    import com.demo.core.utils.exceptions.NonSuccessfullResponse
    import com.google.gson.Gson
    import com.google.gson.JsonDeserializationContext
    import com.google.gson.JsonDeserializer
    import com.google.gson.JsonElement
    import com.google.gson.reflect.TypeToken
    import java.lang.reflect.Type
    
    /**
     * A custom deserializers that uses the generic arg TYPE to deserialize on the fly the json responses from
     * the API.
     */
    class WrapperDeserializer<TYPE>(
        private val castClazz: Class<TYPE>,
        private val isList: Boolean
    ) : JsonDeserializer<TYPE> {
    
        val gson = Gson()
    
        override fun deserialize(
            element: JsonElement,
            arg1: Type,
            arg2: JsonDeserializationContext
        ): TYPE? {
            val jsonObject = element.asJsonObject
    
            if (jsonObject.get("success").asBoolean) {
                return if (isList) {
                    val type = TypeToken.getParameterized(List::class.java, castClazz).type
                    gson.fromJson(jsonObject.get("payload"), type)
                } else {
                    gson.fromJson(jsonObject.get("payload"), castClazz)
                }
            } else {
                throw NonSuccessfullResponse()
            }
        }
    }
    

    然后在您实例化 Gson 实例的任何地方,您都可以执行以下操作:

    fun provideGson(): Gson {
            val bookListType = TypeToken.getParameterized(List::class.java, ApiAvailableBooksResponse::class.java).type
            return GsonBuilder()
                .registerTypeAdapter(bookListType, WrapperDeserializer(ApiAvailableBooksResponse::class.java, true))
                .registerTypeAdapter(ApiProfileInfoResponse::class.java, WrapperDeserializer(ApiProfileInfoResponse::class.java, false))
                .registerTypeAdapter(Date::class.java, DateDeserializer())
                .create()
        }
    

    请注意,我们正在映射两种不同类型的响应,即书籍列表,例如:

    {
      "success": "success",
      "payload": [
        {...}, // Book 1
        {...}, // Book 2
        {...} // Book 3
      ]
    }
    

    以及单个用户配置文件响应:

    {
      "success": "success",
      "payload": {
         "name": "etc",
         // ...
       }
    }
    

    同样,Interceptor 方法是一个非常有趣的选项,我以前没有考虑过 - 它在灵活性方面让我有点担心,因为你强制所有端点响应遵循相同的标准 - 但它看起来喜欢更整洁的方法。

    【讨论】:

      猜你喜欢
      • 2018-03-28
      • 2016-03-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-11-04
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多