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

时间:2017-06-06 21:32:10

标签: java json gson

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

  • 能够委托调用我们自定义实现的默认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_message",它会抛出给定"error"的异常:

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

这项工作要多得多,但至少让我委托给同一个Gson配置。

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

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

我没有看到任何方法,因为我不认为JsonReader有一个选项来读取输入两次,也没有办法将“数据”值缓存在抽象类型中,如遇到“类型”后解析JsonElement

1 个答案:

答案 0 :(得分:2)

  

但实际上它是错误的,因为它没有使用相同的Gson将该工作委托给它,所以它将使用不同的字段命名策略,例如。

正确。您应该使用JsonDeserializationContext

  

...因为我不认为JsonReader有两次读取输入的选项,所以也无法在JsonElement之类的抽象类型中缓存“data”值,以便在遇到“type”后进行解析。 / p>

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

可以使用这两种方法,但我会选择JsonDeserializer以简化(假设你不打算写一个超快速解串器)。

我不确定你的UserApiError是如何相互关联的,但我会选择两种不同类型的公共类:真值和错误。看起来你的两个班级有一个共同的父母或祖先,但我不确定你如何在一个电话站点处理它们(也许是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);
            }
        }
    }
}

输出:

  

成功:隐形粉红独角兽
  错误:您输入的用户名或密码不正确   成功:隐形粉红独角兽
  错误:您输入的用户名或密码不正确。

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

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

是的,不要忘记在这里检查解析状态(以某种方式处理两个案例中缺少data和{{1}})。如您所见,这会引入可变复杂性和性能/内存成本,但它可以为您提供最佳性能。你决定了。