【问题标题】:Json response parser for Array or Object数组或对象的 Json 响应解析器
【发布时间】:2017-03-30 09:08:54
【问题描述】:

我正在编写一个库来使用 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 类,它只会映射错误属性

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 识别这个结构,而不必手动处理这个场景? 有没有更好的方法来实现这一点? 我是否错过了反序列化器可能失败或无法按预期工作的任何情况?

谢谢

【问题讨论】:

    标签: java json gson


    【解决方案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 响应必须被反序列化。请注意,类型适配器与JsonSerializerJsonDeserializer 不同,它以流方式工作,不需要将整个 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);
            }
        }
    
    }
    

    上面的代码会产生:


    约翰 (21)
    莎拉 (32)
    错误:1001 发生故障

    约翰 (21)
    莎拉 (32)
    错误:1001 发生故障

    约翰 (21)
    莎拉 (32)

    【讨论】:

    • 感谢您的详细回答。虽然比我最初想象的要复杂一些,但它让我看到了构建这种库的不同点。我不认为我会实施这种方法,因为我无法访问 HttpResponse,因为我目前正在使用改造,只能通过解析配置来解决这个问题。尽管如此,由于我的问题没有正确/错误的答案,这绝对是最完整的答案,我认为与我的方法一起代表了很多实施策略。
    【解决方案2】:

    在您没有错误的情况下,由于顶级元素是数组而不是对象,因此您必须使用自定义反序列化器。你无法摆脱它。 (我假设您无法更改响应格式。)

    据我所知,使代码更简洁的最佳尝试是创建一个抽象的顶级反序列化器类并在此处检查error。如果没有错误,将解析字段委托给某个抽象方法,该方法将在您为每个类编写的自定义序列化程序中实现。

    【讨论】:

    • 感谢拉姆的回答。通过使用该策略,我将不得不为每个响应类实现自定义反序列化器。虽然这是一个棘手的情况,但大多数情况下,响应是 json 对象,Gson 可以开箱即用地解析这些对象,并带有继承的 ApiResponse 错误。虽然有效,但在我的情况下,建议的策略将需要实际编写更多代码。谢谢
    • 你没有给其他模型。正如我在开始时所说,只有当您的顶级结构是数组时才需要这样做。
    【解决方案3】:

    这个解决方案几乎非常适合这种情况。但我想更一般地定义响应,是否应该有一个状态来识别请求的成功或失败?所以我更喜欢json格式是这样的:

    为了成功:

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

    失败:

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

    【讨论】:

    • 这是理想的场景/模式。但是我无法控制 API 响应,并且此更改不会向后兼容其余客户端。谢谢
    猜你喜欢
    • 1970-01-01
    • 2017-10-12
    • 2019-03-06
    • 2013-09-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-10-18
    相关资源
    最近更新 更多