杰克逊反序列化错误处理

23

我的问题相当简单:我有一个如下的简单类:

public class Foo {
   private int id = -1;
   public void setId(int _id){ this.id = _id; }
   public int getId(){ return this.id; }
}

我正在尝试处理以下JSON:

{
  "id": "blah"
}

很明显,这里存在问题("blah" 无法解析为 int)

以前,Jackson会抛出类似于 org.codehaus.jackson.map.JsonMappingException:无法从字符串值“blah”构建 java.lang.Integer 实例:不是一个有效的整数值

我同意这一点,但我想在某个地方注册一些内容,允许忽略这种类型的映射错误。我尝试了注册 DeserializationProblemHandler(参见此处),但它似乎只适用于未知属性,而不是反序列化问题。

你对这个问题有什么线索吗?


为什么要忽略这个错误?如果有客户端试图用这样的资源表示来“PUT”我,我会向每个客户端返回HTTP 400状态码 :) - user647772
2
我正在使用Jackson与Spring MVC和bean验证。问题是Jackson在到达Spring MVC层之前就会抱怨反序列化问题,因此我无法以一致的方式向客户端发送错误信息。 - Frédéric Camblor
我个人经常使用Jackson将对象转储为可读的日志。能够注意到序列化问题并继续进行非常有帮助。 - Roy Truelove
5个回答

21
我成功解决了我的问题,感谢Jackson ML的Tatu
我不得不为Jackson处理的每种原始类型使用自定义非阻塞反序列化器。就像这样的工厂:
public class JacksonNonBlockingObjectMapperFactory {

    /**
     * Deserializer that won't block if value parsing doesn't match with target type
     * @param <T> Handled type
     */
    private static class NonBlockingDeserializer<T> extends JsonDeserializer<T> {
        private StdDeserializer<T> delegate;

        public NonBlockingDeserializer(StdDeserializer<T> _delegate){
            this.delegate = _delegate;
        }

        @Override
        public T deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
            try {
                return delegate.deserialize(jp, ctxt);
            }catch (JsonMappingException e){
                // If a JSON Mapping occurs, simply returning null instead of blocking things
                return null;
            }
        }
    }

    private List<StdDeserializer> jsonDeserializers = new ArrayList<StdDeserializer>();

    public ObjectMapper createObjectMapper(){
        ObjectMapper objectMapper = new ObjectMapper();

        SimpleModule customJacksonModule = new SimpleModule("customJacksonModule", new Version(1, 0, 0, null));
        for(StdDeserializer jsonDeserializer : jsonDeserializers){
            // Wrapping given deserializers with NonBlockingDeserializer
            customJacksonModule.addDeserializer(jsonDeserializer.getValueClass(), new NonBlockingDeserializer(jsonDeserializer));
        }

        objectMapper.registerModule(customJacksonModule);
        return objectMapper;
    }

    public JacksonNonBlockingObjectMapperFactory setJsonDeserializers(List<StdDeserializer> _jsonDeserializers){
        this.jsonDeserializers = _jsonDeserializers;
        return this;
    }
}

然后以这种方式进行调用(只传递那些你想要非阻塞的反序列化器):

JacksonNonBlockingObjectMapperFactory factory = new JacksonNonBlockingObjectMapperFactory();
factory.setJsonDeserializers(Arrays.asList(new StdDeserializer[]{
    // StdDeserializer, here, comes from Jackson (org.codehaus.jackson.map.deser.StdDeserializer)
    new StdDeserializer.ShortDeserializer(Short.class, null),
    new StdDeserializer.IntegerDeserializer(Integer.class, null),
    new StdDeserializer.CharacterDeserializer(Character.class, null),
    new StdDeserializer.LongDeserializer(Long.class, null),
    new StdDeserializer.FloatDeserializer(Float.class, null),
    new StdDeserializer.DoubleDeserializer(Double.class, null),
    new StdDeserializer.NumberDeserializer(),
    new StdDeserializer.BigDecimalDeserializer(),
    new StdDeserializer.BigIntegerDeserializer(),
    new StdDeserializer.CalendarDeserializer()
}));
ObjectMapper om = factory.createObjectMapper();

12
你可能希望让你的控制器处理这个问题,通过添加一个处理该特定异常的方法。
@ExceptionHandler(HttpMessageNotReadableException.class)
@ResponseBody
public String handleHttpMessageNotReadableException(HttpMessageNotReadableException ex)
{
    JsonMappingException jme = (JsonMappingException) ex.getCause();
    return jme.getPath().get(0).getFieldName() + " invalid";
}

当然,这句话

    JsonMappingException jme = (JsonMappingException) ex.getCause();

有些情况下可能会抛出类转换异常,但我还没有遇到过。


3
你说得对,这个方法是有效的解决方案。然而,它也存在一些缺点:
  • 你将会逐一报告错误(你需要3次请求才能确认你有3个格式不正确的字段)。 在字段格式不正确时放置null值,可以让我在该字段上放置一个@NotNull注释,以便在1个请求中获取完整的错误列表。
  • 这样可以针对每个字段进行特定处理。如果某些字段格式不正确,但这并不关键,我不会在这些字段上放置@NotNull => 错误将被过滤。
- Frédéric Camblor
是的,你说得对,你的方法更好。 我很感兴趣知道你如何使它适用于原始值,因为我希望我的领域模型有一个private int age,而不是private Integer age。 我通过使用 new StdDeserializer.IntegerDeserializer(int.class, 0) 来解决这个问题,并将 deserialize 方法中的 return null 替换为 **return delegate.getNullValue();**。如果你认为这是正确的解决方案,请更新你的代码以包括它,如果你有其他的解决方案,请分享。 - E L
我在想在这种情况下是否考虑使用原始类型是一个好主意。因为对我来说,使用某些值来表示“非法”值(在您的情况下,该值似乎为0)听起来很奇怪。我宁愿使用一个不太常用的值,比如Integer.MAX_VALUE,但即使在这种情况下,我仍然觉得这种行为有点太武断了(这是个人观点)。 - Frédéric Camblor
2
一个简单的检查 if(ex.getCause() instanceof JsonMappingException)(如果不是,则适当的逻辑来重新抛出或委托处理其他异常类型)可以消除任何可能出现 ClassCastException 的情况。否则,答案很有用。 - StormeHawke

2
我编写了一个简单的错误处理程序,它将提供一些错误信息,你可以返回给用户,状态码为“ bad request”。使用 @JsonProperty required = true 来获取与缺少属性相关的错误信息。Jackson 版本2.9.8。
public class JacksonExceptionHandler {

    public String getErrorMessage(HttpMessageNotReadableException e) {
        String message = null;
        boolean handled = false;
        Throwable cause = e.getRootCause();

        if (cause instanceof UnrecognizedPropertyException) {
            UnrecognizedPropertyException exception = (UnrecognizedPropertyException) cause;
            message = handleUnrecognizedPropertyException(exception);
            handled = true;
        }
        if (cause instanceof InvalidFormatException) {
            InvalidFormatException exception = (InvalidFormatException) cause;
            message = handleInvalidFormatException(exception);
            handled = true;
        }
        if (cause instanceof MismatchedInputException) {
            if (!handled) {
                MismatchedInputException exception = (MismatchedInputException) cause;
                message = handleMisMatchInputException(exception);
            }
        }
        if (cause instanceof JsonParseException) {
            message = "Malformed json";
        }
        return message;
    }

    private String handleInvalidFormatException(InvalidFormatException exception) {
        String reference = null;
        if (!exception.getPath().isEmpty()) {
            String path = extractPropertyReference(exception.getPath());
            reference = removeLastCharacter(path);
        }
        Object value = exception.getValue();
        return "Invalid value '" + value + "' for property : " + reference;
    }

    private String handleUnrecognizedPropertyException(UnrecognizedPropertyException exception) {
        String reference = null;
        if (!exception.getPath().isEmpty()) {
            String path = extractPropertyReference(exception.getPath());
            reference = removeLastCharacter(path);
        }
        return "Unknown property : '" + reference + "'";
    }

    private String handleMisMatchInputException(MismatchedInputException exception) {
        String reference = null;
        if (!exception.getPath().isEmpty()) {
            reference = extractPropertyReference(exception.getPath());
        }
        String property = StringUtils.substringBetween(exception.getLocalizedMessage(), "'", "'");
        // if property missing inside nested object
        if (reference != null && property!=null) {
            return "Missing property : '" + reference + property + "'";
        }
        // if invalid value given to array
        if(property==null){
            return "Invalid values at : '"+ reference +"'";
        }
        // if property missing at root level
        else return "Missing property : '" + property + "'";
    }

    // extract nested object name for which property is missing
    private String extractPropertyReference(List<JsonMappingException.Reference> path) {
        StringBuilder stringBuilder = new StringBuilder();
        path.forEach(reference -> {
                    if(reference.getFieldName() != null) {
                        stringBuilder.append(reference.getFieldName()).append(".");
                        // if field is null means it is array
                    } else stringBuilder.append("[].");
                }
                );
        return stringBuilder.toString();
    }

    // remove '.' at the end of property path reference
    private String removeLastCharacter(String string) {
        return string.substring(0, string.length() - 1);
    }
}

可以在全局通知中调用此类对象,如下所示

@Override
    protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
        String message = new JacksonExceptionHandler().generator.getErrorMessage(ex);
        if(message == null){
            return ResponseEntity.badRequest().body("Malformed json");
        }
        return ResponseEntity.badRequest().body(message);
    }

1
创建一个简单的映射器:
@Provider
@Produces(MediaType.APPLICATION_JSON)
public class JSONProcessingErroMapper implements ExceptionMapper<InvalidFormatException> {

@Override
public Response toResponse(InvalidFormatException ex) { 
    return Response.status(400)
             .entity(new ClientError("[User friendly message]"))
             .type(MediaType.APPLICATION_JSON)
             .build();
}

}


0

DeserializationProblemHandler 现在有更多的方法,例如 handleUnexpectedTokenhandleWeird*Value。它应该能够处理任何需要的内容。

继承它,覆盖你需要的方法,然后使用 addHandler(DeserializationProblemHandler h) 将其添加到你的 ObjectMapper 中。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接