2016-05-14 76 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());