Jackson deserialization error handling
Create a simple Mapper:
@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();
}
}
I succeeded to solve my problem, thanks to Tatu from Jackson ML.
I had to use custom non blocking deserializers for every primitive types handled in Jackson. Something like this factory :
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;
}
}
Then calling it like this way (pass as deserializers only those you want to be non blocking) :
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();
I have written a simple error handler which will give you some kind of error which you can return to user with bad request as status code. Use @JsonProperty required = true to get error related to missing properties. Jackson version 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);
}
}
and call this class object in global advice like this
@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);
}
You might want to let your controller handle the problem by adding a method that handles this specific exception
@ExceptionHandler(HttpMessageNotReadableException.class)
@ResponseBody
public String handleHttpMessageNotReadableException(HttpMessageNotReadableException ex)
{
JsonMappingException jme = (JsonMappingException) ex.getCause();
return jme.getPath().get(0).getFieldName() + " invalid";
}
Of course, the line
JsonMappingException jme = (JsonMappingException) ex.getCause();
might throw a class cast exception for some cases but i haven't encountered them yet.