java - 使用 Gson TypeAdapter 以顺序不敏感的方式反序列化 JSON 对象

标签 java json gson

是否有可能实现以下两个目标?

  • 能够委托(delegate)给调用我们的自定义实现的默认 Gson 反序列化器。
  • 不受 JSON 对象中键的不同顺序的影响

下面我描述了两种可能的方法,但只能实现其中一种。

<小时/>

我正在使用的 API 返回成功的结果,例如:

{
  "type": "success",
  "data": {
    "display_name": "Invisible Pink Unicorn",
    "user_email": "user@example.com",
    "user_id": 1234
  }
}

或者一个错误,例如:

{
    "type": "error",
    "data": {
        "error_name": "incorrect_password",
        "error_message": "The username or password you entered is incorrect."
    }
}

目前的处理方式是注册一个 TypeAdapter,如果类型为 “error”,则该 TypeAdapter 会抛出带有给定 “error_message” 的异常:

new GsonBuilder()
    .registerTypeAdapter(User.class, new ContentDeserializer<User>())
    .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
    .create()

public class ContentDeserializer<T> implements JsonDeserializer<T> {
    @Override
    public T deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
        final JsonObject object = json.getAsJsonObject();
        final String type = object.get("type").getAsString();
        final JsonElement data = object.get("data");
        final Gson gson = new Gson();
        if ("error".equals(type)) {
            throw gson.fromJson(data, ApiError.class);
        } else {
            return gson.fromJson(data, typeOfT);
        }
    }
}

这很简洁,因为它非常简洁,并且使用默认的反序列化器来完成所有艰苦的工作。

但实际上这是错误的,因为它不使用相同的 Gson 来委派该工作,因此它会使用不同的字段命名策略,例如。

为了解决这个问题,我编写了一个 TypeAdapterFactory:

public class UserAdapterFactory implements TypeAdapterFactory {

    @SuppressWarnings("unchecked")
    @Override
    public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
        if (!User.class.isAssignableFrom(type.getRawType())) return null;
        final TypeAdapter<User> userAdapter = (TypeAdapter<User>) gson.getDelegateAdapter(this, type);
        final TypeAdapter<ApiError> apiErrorAdapter = gson.getAdapter(ApiError.class);
        return (TypeAdapter<T>) new Adapter(userAdapter, apiErrorAdapter);
    }

    private static class Adapter extends TypeAdapter<User> {
        private final TypeAdapter<User> userAdapter;
        private final TypeAdapter<ApiError> apiErrorAdapter;

        Adapter(TypeAdapter<User> userAdapter, TypeAdapter<ApiError> apiErrorAdapter) {
            this.userAdapter = userAdapter;
            this.apiErrorAdapter = apiErrorAdapter;
        }

        @Override
        public void write(JsonWriter out, User value) throws IOException {
        }

        @Override
        public User read(JsonReader in) throws IOException {
            User user = null;
            String type = null;
            in.beginObject();
            while (in.hasNext()) {
                switch (in.nextName()) {
                    case "type":
                        type = in.nextString();
                        break;
                    case "data":
                        if ("error".equals(type)) {
                            throw apiErrorAdapter.read(in);
                        } else if ("success".equals(type)) {
                            user = userAdapter.read(in);
                        }
                        break;
                }
            }
            in.endObject();
            return user;
        }
    }
}

这需要做更多的工作,但至少让我可以委托(delegate)给相同的 Gson 配置。

这种方法的问题是,当 JSON 对象具有不同的顺序时,它就会中断:

{
  "data": {
    "display_name": "Invisible Pink Unicorn",
    "user_email": "user@example.com",
    "user_id": 1234
  },
  "type": "success"
}

我没有看到任何解决办法,因为我认为 JsonReader 没有读取输入两次的选项,也没有办法将“数据”值缓存在抽象类型,如 JsonElement,在遇到“type”后进行解析。

最佳答案

But actually it's wrong, because it doesn't use the same Gson to delegate that work to, so it will use a different field naming policy, for example.

正确。您应该使用JsonDeserializationContext .

... because I don't think JsonReader has an option to read the input twice, there's also no way to cache the "data" value in an abstract type like JsonElement to parse after "type" has been encountered.

正确。 JsonReader是一个流阅读器,而 JsonElement是一棵树。它们就像 XML 世界中的 SAX 和 DOM,各有优缺点。流式读取器只是读取输入流,您必须自己缓冲/缓存中间数据。

对于您的情况,您可以使用这两种方法,但我会选择 JsonDeserializer因为它的简单性(假设您不打算编写超快的解串器)。

我不太确定你的User怎么样和ApiError彼此相关,但我会为两种不同类型的值使用一个公共(public)类:真实值和错误。看起来你的两个类有一个共同的父类或祖先,但我不太确定你如何在调用站点处理它们(也许instanceof?)。比如说,像这样的东西(隐藏构造函数以封装对象结构初始化的复杂性):

final class Content<T> {

    private final boolean isSuccess;
    private final T data;
    private final ApiError error;

    private Content(final boolean isSuccess, final T data, final ApiError error) {
        this.isSuccess = isSuccess;
        this.data = data;
        this.error = error;
    }

    static <T> Content<T> success(final T data) {
        return new Content<>(true, data, null);
    }

    static <T> Content<T> error(final ApiError error) {
        return new Content<>(false, null, error);
    }

    boolean isSuccess() {
        return isSuccess;
    }

    T getData()
            throws IllegalStateException {
        if ( !isSuccess ) {
            throw new IllegalStateException();
        }
        return data;
    }

    ApiError getError()
            throws IllegalStateException {
        if ( isSuccess ) {
            throw new IllegalStateException();
        }
        return error;
    }

}

两者都是UserApiError从我的角度来看(我更喜欢@SerializedName,尽管这样可以更好地控制命名——但这似乎是一个习惯问题)。

final class ApiError {

    @SuppressWarnings("error_name")
    final String errorName = null;

    @SerializedName("error_message")
    final String errorMessage = null;

}
final class User {

    @SerializedName("display_name")
    final String displayName = null;

    @SerializedName("user_email")
    final String userEmail = null;

    @SuppressWarnings("user_id")
    final int userId = Integer.valueOf(0);

}

接下来,由于树操作更容易,只需实现 JSON 反序列化器即可:

final class ContentJsonDeserializer<T>
        implements JsonDeserializer<Content<T>> {

    // This deserializer holds no state
    private static final JsonDeserializer<?> contentJsonDeserializer = new ContentJsonDeserializer<>();

    private ContentJsonDeserializer() {
    }

    // ... and we hide away that fact not letting this one to be instantiated at call sites
    static <T> JsonDeserializer<T> getContentJsonDeserializer() {
        // Narrowing down the @SuppressWarnings scope -- suppressing warnings for entire method may be harmful
        @SuppressWarnings("unchecked")
        final JsonDeserializer<T> contentJsonDeserializer = (JsonDeserializer<T>) ContentJsonDeserializer.contentJsonDeserializer;
        return contentJsonDeserializer;
    }

    @Override
    public Content<T> deserialize(final JsonElement jsonElement, final Type type, final JsonDeserializationContext context)
            throws JsonParseException {
        final JsonObject jsonObject = jsonElement.getAsJsonObject();
        final String responseType = jsonObject.getAsJsonPrimitive("type").getAsString();
        switch ( responseType ) {
        case "success":
            return success(context.deserialize(jsonObject.get("data"), getTypeParameter0(type)));
        case "error":
            return error(context.deserialize(jsonObject.get("data"), ApiError.class));
        default:
            throw new JsonParseException(responseType);
        }
    }

    // Trying to detect any given type parameterization for its first type parameter
    private static Type getTypeParameter0(final Type type) {
        if ( !(type instanceof ParameterizedType) ) {
            return Object.class;
        }
        return ((ParameterizedType) type).getActualTypeArguments()[0];
    }

}

演示:

private static final Gson gson = new GsonBuilder()
        .registerTypeAdapter(Content.class, getContentJsonDeserializer())
        .create();

private static final Type userContent = new TypeToken<Content<User>>() {
}.getType();

public static void main(final String... args)
        throws IOException {
    for ( final String name : ImmutableList.of("success.json", "error.json", "success-reversed.json", "error-reversed.json") ) {
        try ( final JsonReader jsonReader = getPackageResourceJsonReader(Q44400163.class, name) ) {
            final Content<User> content = gson.fromJson(jsonReader, userContent);
            if ( content.isSuccess() ) {
                System.out.println("SUCCESS: " + content.getData().displayName);
            } else {
                System.out.println("ERROR:   " + content.getError().errorMessage);
            }
        }
    }
}

输出:

SUCCESS: Invisible Pink Unicorn
ERROR: The username or password you entered is incorrect.
SUCCESS: Invisible Pink Unicorn
ERROR: The username or password you entered is incorrect.

现在,回到关于 TypeAdapter 的原始问题。正如我上面提到的,您也可以使用类型适配器来完成此操作,但您必须实现两种情况支持:

  • 转发案例,并且您已经实现了它(最好的案例):阅读type首先属性,然后阅读data根据您的实际日期类型的属性。顺便说一下,你的TypeAdapter实现远非通用:您必须使用 Gson.getDelegateAdapter 解析真实的数据类型及其适配器。 .
  • 相反的情况(最坏的情况):阅读data属性到 TreeView 中(因此将其缓冲到内存中)作为 JsonElement实例(必须先从TypeAdapter<JsonElement>方法中的Gson实例获取create),然后根据下一个type属性值,使用 TypeAdapter.fromJsonTree 将其作为值从树中读取.

是的,不要忘记检查此处的解析状态(以某种方式处理两种情况下缺失的 typedata)。正如您所看到的,这引入了可变的复杂性和性能/内存成本,但它可以为您提供最佳性能。你决定。

关于java - 使用 Gson TypeAdapter 以顺序不敏感的方式反序列化 JSON 对象,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/44400163/

相关文章:

java - 密码加密

java - 将冒泡排序与可比较的 ArrayList 一起使用

java - 如何将 JSONObject 转换为 ArrayList

java - Jackson:如何将平面 json 转换为嵌套 json

java - GSON通过注解控制序列化格式化

java - 为什么对我的实例的静态引用不更新内部值?

java - Spring ThreadPoolTask​​Executor 只运行一个线程

java - 如何检查严格格式的输入?

javascript - 我需要将一个对象序列化为 JSON。我正在使用 jQuery。有没有 "standard"方法可以做到这一点?

android - 使用 GSON 将嵌套对象展平为目标对象