Json response parser for Array or Object

前端 未结 3 1752
北恋
北恋 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 {
    
        // 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);
    
    }
    

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

    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>>() {
        }.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);
            }
        }
    
    }
    

    The code above will produce:


    John (21)
    Sarah (32)
    ERROR: 1001 Something blew up

    John (21)
    Sarah (32)
    ERROR: 1001 Something blew up

    John (21)
    Sarah (32)

提交回复
热议问题