是否有可能同时实现以下两个目标?
- 能够委托给调用自定义实现的默认 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
,因为它的简单性(假设您不打算编写超快速反序列化程序)。
我不太确定您的User
和ApiError
如何相互关联,但我会为两种不同类型的值使用一个公共类:真实值和错误。看起来你的两个类有一个共同的父级或祖先,但我不确定你如何在呼叫站点处理它们(也许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;
}
}
从我的角度来看,User
和ApiError
(我更喜欢@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
将其作为树中的值读取。
是的,不要忘记在这里检查解析状态(以某种方式处理两种情况下的缺失type
和data
)。如您所见,这引入了可变的复杂性和性能/内存成本,但它可以为您提供最佳性能。你决定。
我迟到了,但还有另一种选择。
是的,使用 (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()
方法一起使用。
因此,您可以混合搭配。