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



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

  • 能够委托给调用自定义实现的默认 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",则使用给定"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;
}
}
}

这是更多的工作,但至少让我委派给相同的 Gson 配置。

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

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

而且我看不出有什么办法解决这个问题,因为我认为JsonReader没有读取输入两次的选项,也没有办法将"data"值缓存在抽象类型中,例如JsonElement在遇到"类型"后解析。

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

正确。您应该使用JsonDeserializationContext.

。因为我认为 JsonReader 没有读取两次输入的选项,所以也没有办法将"data"值缓存在像 JsonElement 这样的抽象类型中,以便在遇到"类型"后进行解析。

正确。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实例读入树视图(因此将其缓冲到内存中)(必须先从create方法中的Gson实例获取TypeAdapter<JsonElement>),然后根据下一个type属性值,使用TypeAdapter.fromJsonTree将其作为树中的值读取。

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

我迟到了,但还有另一种选择。

是的,使用 (De)Serializers 很昂贵,因为一次创建大型 JsonTrees 很昂贵,但是......

我目前正在使用 GSON 2.9.1,值得注意的是 JsonParser 有一些有用的静态方法,例如......

JsonReader je = new JsonReader(...);
JsonObject jo = JsonParser.parseReader(jr).getAsJsonObject();

当然,如果你使用 JsonParser 来消费整个 JsonReader,你会得到那棵又大又胖、昂贵的树。 然而,JsonParser有点聪明。因此,如果我们做这样的事情:

JsonReader jr = new JsonReader(...);
jr.beginObject();
jr.nextName();
JsonObject jo = JsonParser.parseReader(jr).getAsJsonObject();

你会发现 JsonObject jo 是 JsonReader 在流中排队的任何内容的子树。 多玩一会儿,你会注意到这个:

JsonReader jr = new JsonReader(...);
jr.beginObject();
jr.nextName();
JsonObject jo = JsonParser.parseReader(jr).getAsJsonObject();
methodCallToProcessJsonObject(jo);
jr.nextName();
jo = JsonParser.parseReader(jr).getAsJsonObject();
methodCallToProcessJsonObject(jo);

设置一些逻辑,您可以使用 Stream 来运行大块 'o JSON,但在更方便的时候切换到处理较小的 JsonElements。

我想你可以用JsonStreamParser做一些非常类似的事情。

在编写器端,JsonWriter 有一个jsonValue(String)方法,您可以将其与 JsonObject 或 JsonArray 及其各自的toString()方法一起使用。

因此,您可以混合搭配。

最新更新