一尘不染

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

json

我正在编写一个使用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<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无需手动处理这种情况就能识别出这种结构?有没有更好的方法可以做到这一点?我是否错过了解串器可能会失败或无法按预期工作的任何情况?

谢谢


阅读 248

收藏
2020-07-27

共1个答案

一尘不染

有时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文档的性能。

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)

2020-07-27