How to make Jackson throw exception as is when deserialization mapping fail

deserialization, jackson, java, json

Solution

Inside of Jackson's `StdValueInstantiator` this method gets hit when an exception is thrown during deserialization:

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);
}

As you can see, this will iterate through each "level" of your nested runtime exceptions and set the last one it hits as the cause for the `JsonMappingException` it returns.

Here is the code I needed to get this working:

Register a new module to the `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) {
    }
}

Create a custom module class.

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());
    }
}

Create a custom `ValueInstantiator` class to override `wrapException(...)`. Add the instantiator to the module.

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);
    }
}

Create a custom deserializer to get the module to work properly. Add this class to the module initialization as well.

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);
    }
}

Problem

Jackson has a weird behavior in handling Exceptions that occur during deserialization mapping: it throws a `JsonMappingException` whose `.getCause()` returns the innermost of the exception chain. ``` //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"))); } } ``` In the code above, I use `jsonMapper.readValue(..)` to map the String `json` to an instance of class `Q` whose the constructor, marked `@JsonCreator`, throws a chain of `RuntimeException`: `"ex 0", "ex 1", "ex 2"`. When the mapping fail, I expected the line `System.out.println(e.getCause());` to print out `ex 0`, but it prints `ex 2`. Why Jackson decides to do this and is there a way to configure it so that it doesn't discard my `ex 0`? I have tried ``` jsonMapper.configure(DeserializationFeature.WRAP_EXCEPTIONS, false); ``` but it doesn't do anything.

Original source