Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
335 views
in Technique[技术] by (71.8m points)

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

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

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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(successfulRe

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...