2

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 of objects if everything goes well like so:

[
  { 
   "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.

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

The problem arises when modeling this in POJOs. 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

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

I decided to still create a class extending 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:

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 = 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? Is there any better way to accomplish this? Am I missing any scenario where the deserializer might fail or not work as expected?

Thanks

3 Answers 3

2

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)

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks for your detailed answer. While a little bit more sophisticated that I initially thought, it allowed me to see a different point of architecting this kind of library. I don't think i will be implementing this approach, as I don't have access to the HttpResponse, because i am using retrofit currently and can only solve this problem with parsing configuration. Nevertheless, since there's no right/wrong answer to my question, this is definitely the most complete answer, and I think that along with my approach represents a good number of strategies for implementations.
1

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.

2 Comments

Thanks for the answer Ram. By using that strategy, i will have to implement custom deserializers for every response class. While this is a tricky case, most of the times, the responses are json objects that are parseable out of the box by Gson with the inherited ApiResponse errors. While valid, the proposed strategy will require to actually write more code in my case. Thanks
You did not give other models. And as I said at the beginning, this is required only if your top level structs are array.
0

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"
     }
  ]
}

1 Comment

This is the ideal scenario/schema. However I can not control the API responses, and this change will not be backward compatible for the rest of the clients. Thanks

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.