使用Gson和Retrofit 2反序列化复杂的API响应

时间:2016-05-14 21:08:39

标签: android gson deserialization retrofit

我正在使用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字段可以是单个对象,也可以是对象列表。
  4. 如何使用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?

4 个答案:

答案 0 :(得分:8)

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

See the gist here for an implementation example.

原始答案:

除非您愿意使用ApiResponseAdapterFactory包装所有服务接口,否则ApiResponse<T>方法不起作用。但是,还有另一种选择:OkHttp拦截器。

这是我们的策略:

  • 对于特定的改造配置,您将注册拦截Response
  • 的应用程序拦截器
  • Response#body()将被反序列化为ApiResponse,我们会返回一个新的ResponseResponseBody就是我们想要的内容。

所以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和Retrofit:

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内容。

答案 1 :(得分: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对象中,并且在Java中转换为Map,因此需要更多的工作来解析它。

答案 2 :(得分: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());

答案 3 :(得分:0)

我必须说,我还没有考虑过将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方法是我以前从未考虑过的一个非常有趣的选项-由于要强制所有端点响应都遵循相同的标准,因此在灵活性方面让我有些担心-但看起来更整洁。