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.