Json response parser for Array or Object

前端 未结 3 1743
北恋
北恋 2020-12-21 12:01

I am writing a library to consume a Json API and I am facing a design problem when using Gson as the parsing library.

One of the endpoints returns an array

相关标签:
3条回答
  • 2020-12-21 12:08

    Sometimes API responses do not fit statically typed languages like Java is very well. 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.

    Is there any way to model this POJOs and have Gson recognize this structure without having to manually handle this scenario?

    No. Gson does not mix objects of different structure, so you still have to tell it your intentions.

    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. 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.

    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)
    Sarah (32)
    ERROR: 1001 Something blew up
    <SEMI CALLBACKS>
    John (21)
    Sarah (32)
    ERROR: 1001 Something blew up
    <NO CALLBACKS>
    John (21)
    Sarah (32)

    0 讨论(0)
  • 2020-12-21 12:20

    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:

    for success:

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

    for failure:

    {
      "status": "failure",
      "errors": [
         { 
           "code": 1001,
           "message": "Something blew up"
         }
      ]
    }
    
    0 讨论(0)
  • 2020-12-21 12:28

    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. 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.

    0 讨论(0)
提交回复
热议问题