我正在编写一个使用Json API的库,并且在使用Gson作为解析库时遇到了设计问题。
array如果一切顺利,则端点之一将返回对象:
array
[ { "name": "John", "age" : 21 }, { "name": "Sarah", "age" : 32 }, ]
但是,API中所有端点的错误模式都是json object而不是数组。
object
{ "errors": [ { "code": 1001, "message": "Something blew up" } ] }
在POJO中对此建模时会出现问题。因为错误模式对于所有API端点都是通用的,所以我决定有一个ApiResponse仅映射errors属性的抽象类。
ApiResponse
public abstract class ApiResponse{ @SerializedName("errors") List<ApiResponseError> errors; } public class ApiResponseError { @SerializedName("code") public Integer code; @SerializedName("message") public String message; }
现在,我想继承自此,ApiResponse以获取“免费”错误映射和每个API端点响应的POJO。但是,此响应的顶级json对象是一个数组(如果服务器成功执行了请求),因此我无法创建一个新类来映射它,就像我想要的那样。
我决定仍然创建一个扩展类ApiResponse:
public class ApiResponsePerson extends ApiResponse { List<Person> persons; }
并实现了一个自定义反序列化器,以根据顶级对象的类型正确解析json,并将其设置为以下类的正确字段:
public class DeserializerApiResponsePerson implements JsonDeserializer<ApiResponsePerson> { @Override public ApiResponse deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { ApiResponsePerson response = new ApiResponsePerson(); if (json.isJsonArray()) { Type personType = new TypeToken<List<Person>>() {}.getType(); response.persons = context.deserialize(json, personType); return response; } if (json.isJsonObject()) { JsonElement errorJson = json.getAsJsonObject().get("errors"); Type errorsType = new TypeToken<List<ApiResponseError>>() {}.getType(); response.errors = context.deserialize(errorJson, errorsType); return response; } throw new JsonParseException("Unexpected Json for 'ApiResponse'"); } }
然后我将其添加到Gson
Gson gson = new GsonBuilder() .registerTypeAdapter(ApiResponsePerson.class, new DeserializerApiResponsePerson()) .create();
有什么方法可以对此POJO进行建模,并使Gson无需手动处理这种情况就能识别出这种结构?有没有更好的方法可以做到这一点?我是否错过了解串器可能会失败或无法按预期工作的任何情况?
谢谢
有时API响应不适合像Java这样的静态类型的语言。我要说的是,如果您遇到的问题是使用不太方便的响应格式,那么如果您希望 方便的 话就必须编写更多代码。在大多数情况下,Gson可以在这种情况下提供帮助,但并非免费提供。
有什么方法可以对此POJO进行建模,并使Gson无需手动处理这种情况就能识别出这种结构?
不会。Gson不会混合使用不同结构的对象,因此您仍然必须告诉它您的意图。
有没有更好的方法可以做到这一点?
我想是的,既可以对响应进行建模,又可以实现解析此类响应的方式。
我是否错过了解串器可能会失败或无法按预期工作的任何情况?
像所有反序列化器一样,它对响应格式也很敏感,因此通常它足够好,但是可以改进。
首先,让我们考虑您只能有两种情况:常规响应和错误。这是一个经典案例,可以这样建模:
abstract class ApiResponse<T> { // A bunch of protected methods, no interface needed as we're considering it's a value type and we don't want to expose any of them protected abstract boolean isSuccessful(); protected abstract T getData() throws UnsupportedOperationException; protected abstract List<ApiResponseError> getErrors() throws UnsupportedOperationException; // Since we can cover all two cases ourselves, let them all be here in this class private ApiResponse() { } static <T> ApiResponse<T> success(final T data) { return new SucceededApiResponse<>(data); } static <T> ApiResponse<T> failure(final List<ApiResponseError> errors) { @SuppressWarnings("unchecked") final ApiResponse<T> castApiResponse = (ApiResponse<T>) new FailedApiResponse(errors); return castApiResponse; } // Despite those three protected methods can be technically public, let's encapsulate the state final void accept(final IApiResponseConsumer<? super T> consumer) { if ( isSuccessful() ) { consumer.acceptSuccess(getData()); } else { consumer.acceptFailure(getErrors()); } } // And make a couple of return-friendly accept methods final T acceptOrNull() { if ( !isSuccessful() ) { return null; } return getData(); } final T acceptOrNull(final Consumer<? super List<ApiResponseError>> errorsConsumer) { if ( !isSuccessful() ) { errorsConsumer.accept(getErrors()); return null; } return getData(); } private static final class SucceededApiResponse<T> extends ApiResponse<T> { private final T data; private SucceededApiResponse(final T data) { this.data = data; } @Override protected boolean isSuccessful() { return true; } @Override protected T getData() { return data; } @Override protected List<ApiResponseError> getErrors() throws UnsupportedOperationException { throw new UnsupportedOperationException(); } } private static final class FailedApiResponse extends ApiResponse<Void> { private final List<ApiResponseError> errors; private FailedApiResponse(final List<ApiResponseError> errors) { this.errors = errors; } @Override protected boolean isSuccessful() { return false; } @Override protected List<ApiResponseError> getErrors() { return errors; } @Override protected Void getData() throws UnsupportedOperationException { throw new UnsupportedOperationException(); } } } interface IApiResponseConsumer<T> { void acceptSuccess(T data); void acceptFailure(List<ApiResponseError> errors); }
一个简单的错误映射:
final class ApiResponseError { // Since incoming DTO are read-only data bags in most-most cases, even getters may be noise here // Gson can strip off the final modifier easily // However, primitive values are inlined by javac, so we're cheating javac with Integer.valueOf final int code = Integer.valueOf(0); final String message = null; }
还有一些值:
final class Person { final String name = null; final int age = Integer.valueOf(0); }
第二个组件是一种特殊类型的适配器来告诉GSON 如何 API响应必须反序列化。请注意,类型适配器不同于以流方式工作JsonSerializer并且JsonDeserializer不需要将整个JSON模型(JsonElement)存储在内存中,因此可以节省内存并提高大型JSON文档的性能。
JsonSerializer
JsonDeserializer
JsonElement
final class ApiResponseTypeAdapterFactory implements TypeAdapterFactory { // No state, so it can be instantiated once private static final TypeAdapterFactory apiResponseTypeAdapterFactory = new ApiResponseTypeAdapterFactory(); // Type tokens are effective value types and can be instantiated once per parameterization private static final TypeToken<List<ApiResponseError>> apiResponseErrorsType = new TypeToken<List<ApiResponseError>>() { }; private ApiResponseTypeAdapterFactory() { } static TypeAdapterFactory getApiResponseTypeAdapterFactory() { return apiResponseTypeAdapterFactory; } @Override public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) { // Is it ApiResponse, a class we can handle? if ( ApiResponse.class.isAssignableFrom(typeToken.getRawType()) ) { // Trying to resolve its parameterization final Type typeParameter = getTypeParameter0(typeToken.getType()); // And asking Gson for the success and failure type adapters to use downstream parsers final TypeAdapter<?> successTypeAdapter = gson.getDelegateAdapter(this, TypeToken.get(typeParameter)); final TypeAdapter<List<ApiResponseError>> failureTypeAdapter = gson.getDelegateAdapter(this, apiResponseErrorsType); @SuppressWarnings("unchecked") final TypeAdapter<T> castTypeAdapter = (TypeAdapter<T>) new ApiResponseTypeAdapter<>(successTypeAdapter, failureTypeAdapter); return castTypeAdapter; } return null; } private static Type getTypeParameter0(final Type type) { // Is this type parameterized? if ( !(type instanceof ParameterizedType) ) { // No, it's raw return Object.class; } final ParameterizedType parameterizedType = (ParameterizedType) type; return parameterizedType.getActualTypeArguments()[0]; } private static final class ApiResponseTypeAdapter<T> extends TypeAdapter<ApiResponse<T>> { private final TypeAdapter<T> successTypeAdapter; private final TypeAdapter<List<ApiResponseError>> failureTypeAdapter; private ApiResponseTypeAdapter(final TypeAdapter<T> successTypeAdapter, final TypeAdapter<List<ApiResponseError>> failureTypeAdapter) { this.successTypeAdapter = successTypeAdapter; this.failureTypeAdapter = failureTypeAdapter; } @Override public void write(final JsonWriter out, final ApiResponse<T> value) throws UnsupportedOperationException { throw new UnsupportedOperationException(); } @Override public ApiResponse<T> read(final JsonReader in) throws IOException { final JsonToken token = in.peek(); switch ( token ) { case BEGIN_ARRAY: // Is it array? Assuming that the responses come as arrays only // Otherwise a more complex parsing is required probably replaced with JsonDeserializer for some cases // So reading the next value (entire array) and wrapping it up in an API response with the success-on state return success(successTypeAdapter.read(in)); case BEGIN_OBJECT: // Otherwise it's probably an error object? in.beginObject(); final String name = in.nextName(); if ( !name.equals("errors") ) { // Let it fail fast, what if a successful response would be here? throw new MalformedJsonException("Expected errors` but was " + name); } // Constructing a failed response object and terminating the error object final ApiResponse<T> failure = failure(failureTypeAdapter.read(in)); in.endObject(); return failure; // A matter of style, but just to show the intention explicitly and make IntelliJ IDEA "switch on enums with missing case" to not report warnings here case END_ARRAY: case END_OBJECT: case NAME: case STRING: case NUMBER: case BOOLEAN: case NULL: case END_DOCUMENT: throw new MalformedJsonException("Unexpected token: " + token); default: throw new AssertionError(token); } } } }
现在,如何将它们放在一起。请注意,响应不会显式地公开其内部,而是要求消费者接受将其私有项真正封装起来。
public final class Q43113283 { private Q43113283() { } private static final String SUCCESS_JSON = "[{\"name\":\"John\",\"age\":21},{\"name\":\"Sarah\",\"age\":32}]"; private static final String FAILURE_JSON = "{\"errors\":[{\"code\":1001,\"message\":\"Something blew up\"}]}"; private static final Gson gson = new GsonBuilder() .registerTypeAdapterFactory(getApiResponseTypeAdapterFactory()) .create(); // Assuming that the Type instance is immutable under the hood so it might be cached private static final Type personsApiResponseType = new TypeToken<ApiResponse<List<Person>>>() { }.getType(); @SuppressWarnings("unchecked") public static void main(final String... args) { final ApiResponse<Iterable<Person>> successfulResponse = gson.fromJson(SUCCESS_JSON, personsApiResponseType); final ApiResponse<Iterable<Person>> failedResponse = gson.fromJson(FAILURE_JSON, personsApiResponseType); useFullyCallbackApproach(successfulResponse, failedResponse); useSemiCallbackApproach(successfulResponse, failedResponse); useNoCallbackApproach(successfulResponse, failedResponse); } private static void useFullyCallbackApproach(final ApiResponse<Iterable<Person>>... responses) { System.out.println("<FULL CALLBACKS>"); final IApiResponseConsumer<Iterable<Person>> handler = new IApiResponseConsumer<Iterable<Person>>() { @Override public void acceptSuccess(final Iterable<Person> people) { dumpPeople(people); } @Override public void acceptFailure(final List<ApiResponseError> errors) { dumpErrors(errors); } }; Stream.of(responses) .forEach(response -> response.accept(handler)); } private static void useSemiCallbackApproach(final ApiResponse<Iterable<Person>>... responses) { System.out.println("<SEMI CALLBACKS>"); Stream.of(responses) .forEach(response -> { final Iterable<Person> people = response.acceptOrNull(Q43113283::dumpErrors); if ( people != null ) { dumpPeople(people); } }); } private static void useNoCallbackApproach(final ApiResponse<Iterable<Person>>... responses) { System.out.println("<NO CALLBACKS>"); Stream.of(responses) .forEach(response -> { final Iterable<Person> people = response.acceptOrNull(); if ( people != null ) { dumpPeople(people); } }); } private static void dumpPeople(final Iterable<Person> people) { for ( final Person person : people ) { System.out.println(person.name + " (" + person.age + ")"); } } private static void dumpErrors(final Iterable<ApiResponseError> errors) { for ( final ApiResponseError error : errors ) { System.err.println("ERROR: " + error.code + " " + error.message); } } }
上面的代码将产生:
John(21) Sarah(32) 错误:1001炸毁了 John(21) Sarah(32) 错误:1001炸毁了 John(21) Sarah(32)