Json响应解析器的Array或Object

我正在编写一个使用Json API的库,当使用Gson作为解析库时,我遇到了设计问题。

其中一个端点返回一个对象array ,如果一切顺利的话:

 [ { "name": "John", "age" : 21 }, { "name": "Sarah", "age" : 32 }, ] 

但是,API中所有端点的错误模式是json object而不是数组。

 { "errors": [ { "code": 1001, "message": "Something blew up" } ] } 

在POJO中对此进行建模时会出现问题。 因为错误模式对于所有API端点都是通用的,所以我决定使用一个抽象的ApiResponse类,它只会映射errors属性

 public abstract class ApiResponse{ @SerializedName("errors") List errors; } public class ApiResponseError { @SerializedName("code") public Integer code; @SerializedName("message") public String message; } 

现在我想从ApiResponseinheritance,以获得“免费”错误映射和每个API端点响应的POJO。 但是,此响应的顶级json对象是一个数组(如果服务器成功执行请求),所以我无法像我希望的那样创建一个新类来映射它。

我决定仍然创建一个扩展ApiResponse的类:

 public class ApiResponsePerson extends ApiResponse { List persons; } 

并实现了一个自定义反序列化器,以根据顶级对象的类型正确解析json,并将其设置为以下类中的正确字段:

 public class DeserializerApiResponsePerson implements JsonDeserializer { @Override public ApiResponse deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { ApiResponsePerson response = new ApiResponsePerson(); if (json.isJsonArray()) { Type personType = new TypeToken<List>() {}.getType(); response.persons = context.deserialize(json, personType); return response; } if (json.isJsonObject()) { JsonElement errorJson = json.getAsJsonObject().get("errors"); Type errorsType = new TypeToken<List>() {}.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 { // 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 getErrors() throws UnsupportedOperationException; // Since we can cover all two cases ourselves, let them all be here in this class private ApiResponse() { } static  ApiResponse success(final T data) { return new SucceededApiResponse<>(data); } static  ApiResponse failure(final List errors) { @SuppressWarnings("unchecked") final ApiResponse castApiResponse = (ApiResponse) new FailedApiResponse(errors); return castApiResponse; } // Despite those three protected methods can be technically public, let's encapsulate the state final void accept(final IApiResponseConsumer 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> errorsConsumer) { if ( !isSuccessful() ) { errorsConsumer.accept(getErrors()); return null; } return getData(); } private static final class SucceededApiResponse extends ApiResponse { 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 getErrors() throws UnsupportedOperationException { throw new UnsupportedOperationException(); } } private static final class FailedApiResponse extends ApiResponse { private final List errors; private FailedApiResponse(final List errors) { this.errors = errors; } @Override protected boolean isSuccessful() { return false; } @Override protected List getErrors() { return errors; } @Override protected Void getData() throws UnsupportedOperationException { throw new UnsupportedOperationException(); } } } 
 interface IApiResponseConsumer { void acceptSuccess(T data); void acceptFailure(List 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响应。 请注意,类型适配器不同于JsonSerializerJsonDeserializer以流方式工作,不需要将整个JSON模型( JsonElement )存储在内存中,因此可以节省内存并提高大型JSON文档的性能。

 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> apiResponseErrorsType = new TypeToken>() { }; private ApiResponseTypeAdapterFactory() { } static TypeAdapterFactory getApiResponseTypeAdapterFactory() { return apiResponseTypeAdapterFactory; } @Override public  TypeAdapter create(final Gson gson, final TypeToken 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> failureTypeAdapter = gson.getDelegateAdapter(this, apiResponseErrorsType); @SuppressWarnings("unchecked") final TypeAdapter castTypeAdapter = (TypeAdapter) 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 extends TypeAdapter> { private final TypeAdapter successTypeAdapter; private final TypeAdapter> failureTypeAdapter; private ApiResponseTypeAdapter(final TypeAdapter successTypeAdapter, final TypeAdapter> failureTypeAdapter) { this.successTypeAdapter = successTypeAdapter; this.failureTypeAdapter = failureTypeAdapter; } @Override public void write(final JsonWriter out, final ApiResponse value) throws UnsupportedOperationException { throw new UnsupportedOperationException(); } @Override public ApiResponse 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 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>>() { }.getType(); @SuppressWarnings("unchecked") public static void main(final String... args) { final ApiResponse> successfulResponse = gson.fromJson(SUCCESS_JSON, personsApiResponseType); final ApiResponse> failedResponse = gson.fromJson(FAILURE_JSON, personsApiResponseType); useFullyCallbackApproach(successfulResponse, failedResponse); useSemiCallbackApproach(successfulResponse, failedResponse); useNoCallbackApproach(successfulResponse, failedResponse); } private static void useFullyCallbackApproach(final ApiResponse>... responses) { System.out.println(""); final IApiResponseConsumer> handler = new IApiResponseConsumer>() { @Override public void acceptSuccess(final Iterable people) { dumpPeople(people); } @Override public void acceptFailure(final List errors) { dumpErrors(errors); } }; Stream.of(responses) .forEach(response -> response.accept(handler)); } private static void useSemiCallbackApproach(final ApiResponse>... responses) { System.out.println(""); Stream.of(responses) .forEach(response -> { final Iterable people = response.acceptOrNull(Q43113283::dumpErrors); if ( people != null ) { dumpPeople(people); } }); } private static void useNoCallbackApproach(final ApiResponse>... responses) { System.out.println(""); Stream.of(responses) .forEach(response -> { final Iterable people = response.acceptOrNull(); if ( people != null ) { dumpPeople(people); } }); } private static void dumpPeople(final Iterable people) { for ( final Person person : people ) { System.out.println(person.name + " (" + person.age + ")"); } } private static void dumpErrors(final Iterable errors) { for ( final ApiResponseError error : errors ) { System.err.println("ERROR: " + error.code + " " + error.message); } } } 

上面的代码将产生:

<完全回电>
约翰(21)
莎拉(32)
错误:1001事情爆炸了

约翰(21)
莎拉(32)
错误:1001事情爆炸了
<没有回电>
约翰(21)
莎拉(32)

在您的无错误情况下,由于顶级元素是数组而不是对象,因此您必须使用自定义反序列化器。 你无法摆脱这种情况。 (我假设您无法更改响应格式。)

据我所知,使代码更清晰的最佳尝试是创建一个抽象的顶级反序列化器类并在此处检查error 。 如果没有错误,请将解析字段委托给一些抽象方法,该方法将在您为每个类编写的自定义序列化程序中实现。

对于这种情况,此解决方案几乎非常有用。 但是我想更一般地定义响应,是否应该有一个状态来识别请求的成功或失败? 所以我更喜欢json格式如下:

成功:

 { "status": "success", "results": [ { "name": "John", "age" : 21 } ] } 

失败:

 { "status": "failure", "errors": [ { "code": 1001, "message": "Something blew up" } ] }