【问题标题】:let Gson throw exceptions on wrong types让 Gson 在错误的类型上抛出异常
【发布时间】:2013-01-09 16:51:50
【问题描述】:

我在我的项目中使用 Gson 将 JSON 字符串反序列化为 Java 对象。如果我提出一个请求,我期望来自服务器的明确响应。服务器要么返回我期望的明确定义的响应,要么返回一个(也定义的)错误对象。

为了清楚起见:假设我有一个像这样的简单对象:

class Dummy{
   private String foo;
   private int bar;
}

还有一个像这样的错误对象:

class ErrorHolder{
   private RequestError error;
}

class RequestError{
    private String publicMsg;
    private String msg;
}

如果我得到像

这样的服务器响应

{"foo":"Hello World", "bar":3 }

一切都按预期进行。

但是如果响应是这样的

{"error":{"publicMsg":"Something bad happened", msg:"you forgot requesting some parameter"}}

我会得到一个Dummy 对象,其中foonullbar 是0! Gson 文档(fromJson)明确指出:

投掷 JsonSyntaxException - 如果 json 不是 classOfT 类型的对象

所以如果我尝试像这样解析第二个响应,我希望得到 JsonSyntaxException:

Dummy dummy = Gson.fromJson(secondResponse, Dummy.class);

因为 Json 不代表 Dummy 对象,而是 ErrorHolder 对象。

所以我的问题是:有没有办法让 Gson 以某种方式检测到错误的类型并抛出异常?

【问题讨论】:

  • 没有使用 JSON Schema,你为什么不先检查你的 JSON 输入以检查它是否有效(即有有效的成员)?
  • Gson 被明确设计为将 JSON 字符串转换为 one 类型,而不是动态猜测您的意思。您使用的对象结构应该反映您希望解析的数据,因此如果error 是您可能收到的字段,它应该是类中的字段。
  • 是否有提供此内置功能的替代 json 解析器?
  • @AbhishekSingh 查看已接受答案的更新。 AnnonatedSerializer 应该可以完成这项工作,而且您可以精确控制发生此行为的字段

标签: java json gson type-safety


【解决方案1】:

不幸的是,那里的文档有点误导。

如果你的类有一个字段的类型与 JSON 中的不匹配,它只会抛出异常,即使这样它也会做一些疯狂的事情来尝试修复它(将 JSON 中的 int 转换为例如,您班级中的String)。如果您的 POJO 中有类似 Date 的字段,并且在 JSON 中遇到了 int,它会抛出它。 JSON 中存在但 POJO 中不存在的字段将被静默忽略,JSON 中缺失但存在于 POJO 中的字段设置为 null

目前,GSON 没有为任何类型的“严格”反序列化提供机制,在这种机制中,您可以为 POJO 中的字段添加 @Required 注释。

在你的情况下......我只是扩展我的 POJO 以包含一个内部错误对象......类似于:

class Dummy {
   private String foo;
   private int bar;
   private Error error;

   private class Error {
        String publicMsg;
        String msg;
   }

   public boolean isError() {
       return error != null;
   }

   // setters and getters for your data, the error msg, etc.
}

您的另一个选择是编写一个自定义反序列化程序,如果 JSON 是错误,则抛出异常,例如:

class MyDeserializer implements JsonDeserializer<Dummy>
{
    @Override
    public Dummy deserialize(JsonElement json, Type typeOfT, 
                              JsonDeserializationContext context)
                    throws JsonParseException
    {
        JsonObject jsonObject = (JsonObject) json;

        if (jsonObject.get("error") != null)
        {
            throw new JsonParseException("Error!");
        }

        return new Gson().fromJson(json, Dummy.class);
    }
} 

编辑添加:最近有人对此表示赞同并重新阅读它,我想“嗯,你知道,你可以自己做这个,它可能会很方便”。

这是一个可重复使用的反序列化器和注释,它将完全满足 OP 的要求。限制是,如果 POJO 需要按原样自定义反序列化器,您必须走得更远,要么在构造函数中传入 Gson 对象以反序列化为对象本身,要么将注解检出到单独的方法并在您的反序列化器中使用它。您还可以通过创建自己的异常并将其传递给JsonParseException 来改进异常处理,以便可以通过调用者中的getCause() 检测到它。

总而言之,在绝大多数情况下,这将起作用:

public class App
{

    public static void main(String[] args)
    {
        Gson gson =
            new GsonBuilder()
            .registerTypeAdapter(TestAnnotationBean.class, new AnnotatedDeserializer<TestAnnotationBean>())
            .create();

        String json = "{\"foo\":\"This is foo\",\"bar\":\"this is bar\"}";
        TestAnnotationBean tab = gson.fromJson(json, TestAnnotationBean.class);
        System.out.println(tab.foo);
        System.out.println(tab.bar);

        json = "{\"foo\":\"This is foo\"}";
        tab = gson.fromJson(json, TestAnnotationBean.class);
        System.out.println(tab.foo);
        System.out.println(tab.bar);

        json = "{\"bar\":\"This is bar\"}";
        tab = gson.fromJson(json, TestAnnotationBean.class);
        System.out.println(tab.foo);
        System.out.println(tab.bar);
    }
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface JsonRequired
{
}

class TestAnnotationBean
{
    @JsonRequired public String foo;
    public String bar;
}

class AnnotatedDeserializer<T> implements JsonDeserializer<T>
{

    public T deserialize(JsonElement je, Type type, JsonDeserializationContext jdc) throws JsonParseException
    {
        T pojo = new Gson().fromJson(je, type);

        Field[] fields = pojo.getClass().getDeclaredFields();
        for (Field f : fields)
        {
            if (f.getAnnotation(JsonRequired.class) != null)
            {
                try
                {
                    f.setAccessible(true);
                    if (f.get(pojo) == null)
                    {
                        throw new JsonParseException("Missing field in JSON: " + f.getName());
                    }
                }
                catch (IllegalArgumentException ex)
                {
                    Logger.getLogger(AnnotatedDeserializer.class.getName()).log(Level.SEVERE, null, ex);
                }
                catch (IllegalAccessException ex)
                {
                    Logger.getLogger(AnnotatedDeserializer.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        }
        return pojo;

    }
}

输出:

这是富 这是酒吧 这是富 空值 线程“主”com.google.gson.JsonParseException 中的异常:JSON 中缺少字段:foo

【讨论】:

  • 这很好,但它似乎不适用于嵌套对象。有什么想法吗?
【解决方案2】:

我创建了 Brian 解决方案的更新版本,它处理嵌套对象并进行了一些其他小的更改。该代码还包括一个更简单的构建器,用于创建 Gson 对象,这些对象可以识别具有用 JsonRequired 注释的字段的类。

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.util.List;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.Lists;
import com.google.common.primitives.Primitives;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;

public class AnnotatedDeserializer<T> implements JsonDeserializer<T> {

private final Gson gson = new Gson();

public T deserialize(JsonElement je, Type type, JsonDeserializationContext jdc) throws JsonParseException {

    T target = gson.fromJson(je, type);
    checkRequired(target);
    return target;
}

private List<Field> findMissingFields(Object target, List<Field> invalidFields) {

    for (Field field : target.getClass().getDeclaredFields()) {
        if (field.getAnnotation(JsonRequired.class) != null) {

            Object fieldValue = ReflectionUtil.getFieldValue(target, field);

            if (fieldValue == null) {
                invalidFields.add(field);
                continue;
            }

            if (!isPrimitive(fieldValue)) {
                findMissingFields(fieldValue, invalidFields);
            }
        }
    }
    return invalidFields;
}

private void checkRequired(Object target) {

    List<Field> invalidFields = Lists.newArrayList();
    findMissingFields(target, invalidFields);

    if (!invalidFields.isEmpty()) {
        throw new JsonParseException("Missing JSON required fields: {"
                + FluentIterable.from(invalidFields).transform(toMessage).join(Joiner.on(", ")) + "}");
    }
}

static Function<Field, String> toMessage = new Function<Field, String>() {
    @Override
    public String apply(Field field) {
        return field.getDeclaringClass().getName() + "/" + field.getName();
    }
};

private boolean isPrimitive(Object target) {

    for (Class<?> primitiveClass : Primitives.allPrimitiveTypes()) {
        if (primitiveClass.equals(target.getClass())) {
            return true;
        }
    }
    return false;
}

public static class RequiredFieldAwareGsonBuilder {

    private GsonBuilder gsonBuilder;

    private RequiredFieldAwareGsonBuilder(GsonBuilder gsonBuilder) {
        this.gsonBuilder = gsonBuilder;
    }

    public static RequiredFieldAwareGsonBuilder builder() {
        return new RequiredFieldAwareGsonBuilder(new GsonBuilder());
    }

    public <T> RequiredFieldAwareGsonBuilder withRequiredFieldAwareType(Class<T> classOfT) {
        gsonBuilder.registerTypeAdapter(classOfT, new AnnotatedDeserializer<T>());
        return this;
    }

    public Gson build() {
        return gsonBuilder.create();
    }
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public static @interface JsonRequired {
}
}

还有反射实用程序

import java.lang.reflect.Field;

public final class ReflectionUtil {

private ReflectionUtil() {
}

public static Object getFieldValue(Object target, Field field) {
    try {
        boolean originalFlag = changeAccessibleFlag(field);
        Object fieldValue = field.get(target);
        restoreAccessibleFlag(field, originalFlag);
        return fieldValue;
    } catch (IllegalAccessException e) {
        throw new RuntimeException("Failed to access field " + field.getDeclaringClass().getName() + "/"
                + field.getName(), e);
    }
}

private static void restoreAccessibleFlag(Field field, boolean flag) {
    field.setAccessible(flag);
}

private static boolean changeAccessibleFlag(Field field) {
    boolean flag = field.isAccessible();
    field.setAccessible(true);
    return flag;
}
}

如果你使用 Guice,你可以在你的模块中添加类似的东西来注入 Gson 对象

@Provides
@Singleton
static Gson provideGson() {
    return RequiredFieldAwareGsonBuilder.builder().withRequiredFieldAwareType(MyType1.class)
            .withRequiredFieldAwareType(MyType2.class).build();
}

【讨论】:

  • 这真的很酷,但是如果你说给定对象的数组,这不会检查数组元素中缺少的字段,对吧?
【解决方案3】:

我不喜欢所选的解决方案。它有效,但这不是使用 Gson 的方式。 Gson 将特定的 JSON 模式映射到对象,反之亦然。理想情况下,您使用的 JSON 格式正确(因此,如果您可以控制 JSON 格式,请考虑更改它),但如果没有,您应该设计解析对象来处理您希望收到的所有情况。

有时您确实需要编写自定义JsonDeserializer,但这不是其中之一。发送消息错误是一种非常标准的做法,通过正确的数据结构,GSON 可以直接处理这样一个简单的用例。

如果您可以控制 JSON 架构

请考虑这样的事情:

{
  "message": {
    "foo": "Hello World",
    "bar": 3
  },
  "error": null;
}

{
  "message": null,
  "error": {
    "publicMsg": "Something bad happened",
    "msg": "you forgot requesting some parameter"
  }
}

请注意,您现在可以定义一个干净的包装类,提供 Dummy 对象(如果可能):

public class JsonResponse {
  private Dummy message;
  private RequestError error;

  public boolean hasError() { return error != null; }
  public Dummy getDummy() {
    Preconditions.checkState(!hasError());
    return message;
  }
  public RequestError getError() {
    Preconditions.checkState(hasError());
    return error;
  }
}

如果你必须处理现有的 JSON 模式

如果你不能重构架构,你必须重构解析类,它看起来像这样:

public class JsonResponse {
  private String foo;
  private int bar;

  private RequestError error;

  public boolean hasError() { return error != null; }
  public Dummy getDummy() {
    Preconditions.checkState(!hasError());
    return new Dummy(foo, bar);
  }
  public RequestError getError() {
    Preconditions.checkState(hasError());
    return error;
  }
}

这不如修复架构可取,但无论哪种方式,您都可以获得相同的通用 API - 调用 hasError() 以查看请求是否成功,然后根据需要调用 getDummy()getError()。调用其他方法(例如,收到错误时的 getDummy())将快速失败。

【讨论】:

  • 这不是一个很好的解决方案,如果您无法控制 json,并且您使用的服务器有点糟糕(并不总是给出记录的 json)
  • 如果你没有能力预测你试图解析的 JSON 的结构,我不知道你有多大希望。
  • 我最终没有使用 GSon,自己解析 Json 并在适当的地方抛出错误
  • 是的,这可能是你能做的最好的了。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-11-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多