如何在反序列化映射失败时使Jackson抛出异常

时间:2014-06-04 14:50:22

标签: java json jackson deserialization

Jackson在处理反序列化映射期间发生的异常时有一种奇怪的行为:它抛出JsonMappingException,其.getCause()返回异常链的最内层。

//in main
ObjectMapper jsonMapper = new ObjectMapper();
String json = "{\"id\": 1}";
try {
    Q q = jsonMapper.readValue(json, Q.class);
} catch (JsonMappingException e) {
    System.out.println(e.getCause()); //java.lang.RuntimeException: ex 2
}

//class Q
public class Q {
    @JsonCreator
    public Q(@JsonProperty("id") int id) {
        throw new RuntimeException("ex 0", 
            new RuntimeException("ex 1", 
                new RuntimeException("ex 2")));
    }
}

在上面的代码中,我使用jsonMapper.readValue(..)将字符串json映射到类Q的实例,其构造函数标记为@JsonCreator,引发{{ {1}}:RuntimeException。如果映射失败,我希望第"ex 0", "ex 1", "ex 2"行打印出System.out.println(e.getCause());,但会打印ex 0

为什么杰克逊决定这样做,有没有办法配置它以便它不会丢弃我的ex 2?我试过了

ex 0

但它没有做任何事情。

2 个答案:

答案 0 :(得分:6)

在杰克逊的StdValueInstantiator内部,当反序列化期间抛出异常时,此方法会被点击:

protected JsonMappingException wrapException(Throwable t)
{
    while (t.getCause() != null) {
        t = t.getCause();
    }
    if (t instanceof JsonMappingException) {
        return (JsonMappingException) t;
    }
    return new JsonMappingException("Instantiation of "+getValueTypeDesc()+" value failed: "+t.getMessage(), t);
}

正如您所看到的,这将遍历每个"级别"嵌套运行时异常的设置,并将它命中的最后一个设置为它返回的JsonMappingException的原因。

以下是我需要的代码:

  1. 将新模块注册到ObjectMapper

    @Test
    public void testJackson() {
        ObjectMapper jsonMapper = new ObjectMapper();
        jsonMapper.registerModule(new MyModule(jsonMapper.getDeserializationConfig()));
        String json = "{\"id\": \"1\"}";
        try {
            Q q = jsonMapper.readValue(json, Q.class);
            System.out.println(q.getId());
        } catch (JsonMappingException e) {
            System.out.println(e.getCause()); //java.lang.RuntimeException: ex 2
        } catch (JsonParseException e) {
        } catch (IOException e) {
        }
    }
    
  2. 创建自定义模块类。

    public class MyModule extends SimpleModule {
        public MyModule(DeserializationConfig deserializationConfig) {
            super("MyModule", ModuleVersion.instance.version());
            addValueInstantiator(Q.class, new MyValueInstantiator(deserializationConfig, Q.class));
            addDeserializer(Q.class, new CustomDeserializer());
        }
    }
    
  3. 创建自定义ValueInstantiator类以覆盖wrapException(...)。将实例化器添加到模块中。

    public class MyValueInstantiator extends StdValueInstantiator {
        public MyValueInstantiator(DeserializationConfig config, Class<?> valueType) {
            super(config, valueType);
        }
    
        @Override
        protected JsonMappingException wrapException(Throwable t) {
            if (t instanceof JsonMappingException) {
                return (JsonMappingException) t;
            }
            return new JsonMappingException("Instantiation of "+getValueTypeDesc()+" value failed: "+t.getMessage(), t);
        }
    }
    
  4. 创建自定义反序列化器以使模块正常工作。将此类添加到模块初始化中。

    public class CustomDeserializer extends StdScalarDeserializer<Q> {
        public CustomDeserializer() {
            super(Q.class);
        }
    
        @Override
        public Q deserialize(JsonParser jp, DeserializationContext context) throws IOException {
            JsonNode node = jp.getCodec().readTree(jp);
            return new Q(node.get("id").asText());
        }
    
        @Override
        public Object deserializeWithType(JsonParser jp, DeserializationContext ctxt, TypeDeserializer typeDeserializer) throws IOException {
            return deserialize(jp, ctxt);
        }
    }
    

答案 1 :(得分:0)

对于任何寻求其他解决方案的人来说,这在Spring Boot 2.2.8.RELEASE上对我有用。注意:这是一个示例,当您有一个带有请求主体的rest控制器,该请求主体具有一个枚举,并且客户端可能发送错误的字段字符串性别,并且您想提供适当的错误消息时:

@PostMapping
public ResponseEntity<ProfileResponse> updateProfile(@RequestBody @Valid ProfileRequest profileRequest) {
    
    ProfileResponse profile = //do something important here that returns profile object response
    return ResponseEntity
            .status(HttpStatus.OK)
            .body(profile);
}

ProfileRequest看起来像

@Data //Lombok getters and setters
public class ProfileRequest{
    private GenderEnum gender;
    //some more attributes
}

将此属性添加到aplication.properties文件中,以确保我们的自定义异常GlobalRuntimeException(有关定义,请参见下文)不会包装在JsonMappingException异常中。

spring.jackson.deserialization.WRAP_EXCEPTIONS=false

然后创建一个类,该类将由Spring Boot自动为其创建一个bean(这将用于反序列化枚举类型的字段性别)。如果找不到枚举,则知道会抛出错误。

@JsonComponent
public class GenderEnumDeserializer extends JsonDeserializer<GenderEnum> {

    @Override
    public GenderEnum deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        String val = p.getValueAsString();
        GenderEnum genderEnum = GenderEnum.fromName(val);
        if(genderEnum == null){
            throw new GlobalRuntimeException("Invalid gender provided. Valid values are MALE | FEMALE | OTHER");
        }
        return genderEnum;
    }

}

GenderEnum中的“ forName”方法如下所示。

public static GenderEnum fromName(String name) {
    GenderEnum foundGenderEnum = null;
    for (GenderEnum genderEnum : values()) {
        if (genderEnum.name().equalsIgnoreCase(name)) {
            foundGenderEnum = genderEnum;
        }
    }
    return foundGenderEnum;
}

然后我们将在ControllerAdvice中设置以捕获GlobalRuntimeException:

@ResponseBody
@ExceptionHandler(GlobalRuntimeException.class)
ResponseEntity<?> handleInvalidGlobalRuntimeException(HttpServletRequest request, GlobalRuntimeException ex) {
    LOGGER.error("Error " + ex);
    return ResponseEntity
            .status(HttpStatus.BAD_REQUEST)
            .body(new ErrorMessage(ex.getCustomMessage));
}

就是这样。