2016-05-14 73 views
9

我使用的是Retrofit 2Gson,并且我在解析API反应时遇到了麻烦。这是我的场景:使用Gson和Retrofit 2来反序列化复杂的API响应

我有一个名为Employee的模型对象,它有三个字段:id,name,age

我有一个返回单一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响应以通用包装器的形式返回,其中的重要部分e data字段。
  2. API返回在不直接对应于在模型中的字段(例如,从id_to_age需要采取的值被映射到age字段型号)
  3. 在该data字段的格式的对象API响应可以是单个对象,也可以是对象列表。

我该如何使用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?

+0

你说 “我的API”。如果您有权访问后端,则应该在服务器端更好地对年龄和名称进行序列化。 – iagreen

+0

我没有访问权限。通过“我的API”,我指的是我正在使用的API。 – user2393462435

+0

为什么不创建表示您的JSON响应的Plain Old Java对象,然后将这些对象映射到您的Employee类? –

回答

7

编辑:相关更新:创建一个自定义转换器工厂没有工作 - 的关键在于避免通过ApiResponseConverterFactory的无限循环是调用改造的nextResponseBodyConverter它允许你指定一个工厂跳过。关键是这将是一个Converter.Factory注册改造,而不是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(); 

而且EmployeeEmployeeResponse应遵循the adapter factory construct I wrote in the previous question。现在所有的ApiResponse字段都应该被拦截器使用,并且您所做的每个Retrofit调用都只应返回您感兴趣的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); 
    } 
} 

与响应的问题是,nameage包含一个JSON对象的内部whitch转换为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());