简体   繁体   English

用于数组或对象的Json响应解析器

[英]Json response parser for Array or Object

I am writing a library to consume a Json API and I am facing a design problem when using Gson as the parsing library. 我正在编写一个使用Json API的库,并且在使用Gson作为解析库时遇到了设计问题。

One of the endpoints returns an array of objects if everything goes well like so: 如果一切顺利,则端点之一将返回对象array

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

However, the error schema for all the endpoints in the API is an json object instead of an array. 但是,API中所有端点的错误模式都是json object而不是数组。

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

The problem arises when modeling this in POJOs. 在POJO中对此建模时会出现问题。 Because the error schema is common for all the API endpoints, I decided to have an abstract ApiResponse class which will only map the errors attribute 因为错误模式对于所有API端点都是通用的,所以我决定有一个抽象的ApiResponse类,该类将仅映射errors属性

public abstract class ApiResponse{

  @SerializedName("errors")
  List<ApiResponseError> errors;
}

public class ApiResponseError {

  @SerializedName("code")
  public Integer code;

  @SerializedName("message")
  public String message;
} 

Now I would like to inherit from ApiResponse to have the error mapping "for free" and a POJO per API endpoint response. 现在,我想从ApiResponse继承以具有“免费”错误映射和每个API端点响应的POJO。 However, the top level json object for this response is an array (if the server succeeds to execute the request), so I can not create a new class to map it like I would like it. 但是,此响应的顶级json对象是一个数组(如果服务器成功执行了请求),因此我无法创建一个新类来映射它(如我所愿)。

I decided to still create a class extending ApiResponse : 我决定仍然创建一个扩展ApiResponse的类:

public class ApiResponsePerson extends ApiResponse {

  List<Person> persons;
}

And implemented a custom deserializer to correctly parse the json depending on the type of the top level object, and setting it to the correct field on the following class: 并实现了一个自定义反序列化器,以根据顶级对象的类型正确解析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'");
  }
}

Which I will then add to the Gson 然后我将其添加到Gson

Gson gson = new GsonBuilder()
    .registerTypeAdapter(ApiResponsePerson.class, new DeserializerApiResponsePerson())
    .create();

Is there any way to model this POJOs and have Gson recognize this structure without having to manually handle this scenario? 有什么方法可以对此POJO进行建模,并让Gson无需手动处理这种情况就可以识别这种结构? Is there any better way to accomplish this? 有没有更好的方法可以做到这一点? Am I missing any scenario where the deserializer might fail or not work as expected? 我是否错过了解串器可能会失败或无法按预期工作的任何情况?

Thanks 谢谢

Sometimes API responses do not fit statically typed languages like Java is very well. 有时API响应不适合像Java这样的静态类型的语言。 I would say that if you're facing a problem to align with a not very convenient response format, you have to write more code if you want it to be convenient for you. 我要说的是,如果您遇到的问题是使用不太方便的响应格式,那么如果您希望方便的话就必须编写更多代码。 And in most cases Gson can help in such cases, but not for free. 在大多数情况下,Gson可以在这种情况下提供帮助,但并非免费提供。

Is there any way to model this POJOs and have Gson recognize this structure without having to manually handle this scenario? 有什么方法可以对此POJO进行建模,并让Gson无需手动处理这种情况就可以识别这种结构?

No. Gson does not mix objects of different structure, so you still have to tell it your intentions. 不会。Gson不会混合使用不同结构的对象,因此您仍然必须告诉它您的意图。

Is there any better way to accomplish this? 有没有更好的方法可以做到这一点?

I guess yes, for both modelling the response and implementing the way how such responses are parsed. 我想是的,既可以对响应进行建模,又可以实现解析响应的方式。

Am I missing any scenario where the deserializer might fail or not work as expected? 我是否错过了解串器可能会失败或无法按预期工作的任何情况?

It's response format sensitive like all deserializers are, so in general it's good enough, but can be improved. 像所有反序列化器一样,它对响应格式也很敏感,因此通常它足够好,但是可以改进。

First off, let's consider you can have two cases only: a regular response and an error. 首先,让我们考虑您只能有两种情况:常规响应和错误。 This is a classic case, and it can be modelled like that: 这是一个经典案例,可以这样建模:

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);

}

A trivial mapping for 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;

}

And some values too: 还有一些值:

final class Person {

    final String name = null;
    final int age = Integer.valueOf(0);

}

The second component is a special type adapter to tell Gson how the API responses must be deserialized. 第二个组件是一种特殊类型的适配器来告诉GSON API响应必须如何反序列化。 Note that type adapter, unlike JsonSerializer and JsonDeserializer work in streaming fashion not requiring the whole JSON model ( JsonElement ) to be stored in memory, thus you can save memory and improve the performance for large JSON documents. 请注意,与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<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);
            }
        }

    }

}

Now, how it all can be put together. 现在,如何将它们放在一起。 Note that the responses do not expose their internals explicitly but rather requiring consumers to accept making its privates really encapsulated. 注意,响应不会显式地公开其内部,而是要求消费者接受将其私有项真正封装起来。

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);
        }
    }

}

The code above will produce: 上面的代码将产生:

<FULL CALLBACKS> <全程回调>
John (21) 约翰(21)
Sarah (32) 莎拉(32)
ERROR: 1001 Something blew up 错误:1001发生故障
<SEMI CALLBACKS> <半回调>
John (21) 约翰(21)
Sarah (32) 莎拉(32)
ERROR: 1001 Something blew up 错误:1001发生故障
<NO CALLBACKS> <没有回呼>
John (21) 约翰(21)
Sarah (32) 莎拉(32)

In your error-free case, since the top-level element is an array rather than an object, you have to use custom deserializers. 在没有错误的情况下,由于顶级元素是数组而不是对象,因此必须使用自定义反序列化器。 You cannot escape from that. 您无法逃脱。 (I assume you cannot change the response formats.) (我假设您不能更改响应格式。)

The best attempt to make the code cleaner, as far as I can see, is to create an abstract top-level deserializer class and check for error here. 据我所知,使代码更整洁的最佳尝试是创建一个抽象的顶级反序列化器类,并在此处检查error If there is no error, delegate parsing fields to some abstract method which will be implemented in custom serializers that you have written for each class. 如果没有错误,则将解析字段委托给某种抽象方法,该方法将在您为每个类编写的自定义序列化程序中实现。

This solution is almost very good for this scenario. 对于这种情况,此解决方案几乎是非常好的。 But I would like to define the response more general, is there should be a status to identify success or failure for the request? 但是我想将响应定义得更笼统,是否应该有一个状态来标识请求的成功或失败? So I prefer the json format to be like this: 所以我更喜欢json格式是这样的:

for success: 成功:

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

for failure: 对于失败:

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM