diff --git a/src/main/java/tools/jackson/databind/DeserializationContext.java b/src/main/java/tools/jackson/databind/DeserializationContext.java index 6366cdd07f..0aa36279ab 100644 --- a/src/main/java/tools/jackson/databind/DeserializationContext.java +++ b/src/main/java/tools/jackson/databind/DeserializationContext.java @@ -1354,6 +1354,45 @@ public Object handleWeirdStringValue(Class targetClass, String value, throw weirdStringException(value, targetClass, msg); } + /** + * Method that deserializers should call if they encounter a null value and + * target value type is a Primitive type. + * + * Default implementation will try to call {@link DeserializationContext#reportInputMismatch(Class, String, Object...)}, + * which by default would throw {@link MismatchedInputException} + * + * @param targetClass Primitive type into which incoming {@code null} value should be converted to + * @param deser Type of {@link ValueDeserializer} calling this method + * @param msg Error message template caller wants to use if exception is to be thrown + * + * @throws JacksonException To indicate unrecoverable problem, usually based on msg + */ + public Object handleNullForPrimitives(Class targetClass, + ValueDeserializer deser, String msg) + throws JacksonException + { + // but if not handled, just throw exception + LinkedNode h = _config.getProblemHandlers(); + while (h != null) { + // Can bail out if it's handled + Object instance = h.value().handleNullForPrimitives(this, targetClass, deser, _parser, msg); + if (instance != DeserializationProblemHandler.NOT_HANDLED) { + // Sanity check for broken handlers, otherwise nasty to debug: + if (_isCompatible(targetClass, instance)) { + return instance; + } + // In case our problem handler providing incompatible value, + throw new InvalidFormatException(_parser, + String.format("Cannot deserialize value of type %s from type %s", + targetClass, ClassUtil.getClassDescription(instance)), + instance, targetClass + ); + } + h = h.next(); + } + return reportInputMismatch(deser, msg); + } + /** * Method that deserializers should call if they encounter a numeric value * that cannot be converted to target property type, in cases where some diff --git a/src/main/java/tools/jackson/databind/deser/DeserializationProblemHandler.java b/src/main/java/tools/jackson/databind/deser/DeserializationProblemHandler.java index 8cd736b745..fa84a284de 100644 --- a/src/main/java/tools/jackson/databind/deser/DeserializationProblemHandler.java +++ b/src/main/java/tools/jackson/databind/deser/DeserializationProblemHandler.java @@ -7,6 +7,7 @@ import tools.jackson.core.JsonToken; import tools.jackson.databind.*; import tools.jackson.databind.jsontype.TypeIdResolver; +import tools.jackson.databind.util.ClassUtil; /** * This is the class that can be registered (via @@ -221,6 +222,43 @@ public Object handleUnexpectedToken(DeserializationContext ctxt, return NOT_HANDLED; } + /** + * Method that deserializers should call if the {@code null} value is encountered and + * need to deserialize as a Primitive types (e.g. int, long etc...) that is, type of token that deserializer + * cannot handle). This could occur, for example, if a Number deserializer + * encounter {@link JsonToken#START_ARRAY} instead of + * {@link JsonToken#VALUE_NUMBER_INT} or {@link JsonToken#VALUE_NUMBER_FLOAT}. + * + * + * @param ctxt + * @param targetType Target type to deserialize into + * @param deser Target deserializer that attempted to deserialize {@code null} value in question + * @param p JsonParser used + * @param failureMsg Message that will be used by caller to indicate type of failure unless + * handler produces value to use + * + * + * @return Either {@link #NOT_HANDLED} to indicate that handler does not know + * what to do (and exception may be thrown), or value to use (possibly + * null + */ + public Object handleNullForPrimitives(DeserializationContext ctxt, + Class targetType, ValueDeserializer deser, JsonParser p, String failureMsg) + throws JacksonException + { + return NOT_HANDLED; + } + /** * Method called when instance creation for a type fails due to an exception. * Handler may choose to do one of following things: diff --git a/src/main/java/tools/jackson/databind/deser/jdk/NumberDeserializers.java b/src/main/java/tools/jackson/databind/deser/jdk/NumberDeserializers.java index ce2303f14c..8f6eabcea5 100644 --- a/src/main/java/tools/jackson/databind/deser/jdk/NumberDeserializers.java +++ b/src/main/java/tools/jackson/databind/deser/jdk/NumberDeserializers.java @@ -159,14 +159,16 @@ public AccessPattern getNullAccessPattern() { return AccessPattern.CONSTANT; } + @SuppressWarnings("unchecked") @Override public final T getNullValue(DeserializationContext ctxt) { // 01-Mar-2017, tatu: Alas, not all paths lead to `_coerceNull()`, as `SettableBeanProperty` // short-circuits `null` handling. Hence need this check as well. if (_primitive && ctxt.isEnabled(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES)) { - ctxt.reportInputMismatch(this, - "Cannot map `null` into type %s (set DeserializationConfig.DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES to 'false' to allow)", - ClassUtil.classNameOf(handledType())); + return (T) ctxt.handleNullForPrimitives(handledType(), this, + String.format("Cannot map `null` into type %s (set DeserializationConfig.DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES to 'false' to allow)", + ClassUtil.classNameOf(handledType())) + ); } return _nullValue; } diff --git a/src/main/java/tools/jackson/databind/deser/std/StdDeserializer.java b/src/main/java/tools/jackson/databind/deser/std/StdDeserializer.java index fbee204ca5..63126c0395 100644 --- a/src/main/java/tools/jackson/databind/deser/std/StdDeserializer.java +++ b/src/main/java/tools/jackson/databind/deser/std/StdDeserializer.java @@ -1638,9 +1638,10 @@ protected final void _verifyNullForPrimitive(DeserializationContext ctxt) throws DatabindException { if (ctxt.isEnabled(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES)) { - ctxt.reportInputMismatch(this, -"Cannot coerce `null` to %s (disable `DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES` to allow)", - _coercedTypeDesc()); + ctxt.handleNullForPrimitives(handledType(), + this, + String.format("Cannot coerce `null` to %s (disable `DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES` to allow)", + _coercedTypeDesc())); } } diff --git a/src/test/java/tools/jackson/databind/deser/filter/DeserializationProblemHandler5469Test.java b/src/test/java/tools/jackson/databind/deser/filter/DeserializationProblemHandler5469Test.java new file mode 100644 index 0000000000..d68e40ce8e --- /dev/null +++ b/src/test/java/tools/jackson/databind/deser/filter/DeserializationProblemHandler5469Test.java @@ -0,0 +1,102 @@ +package tools.jackson.databind.deser.filter; + +import org.junit.jupiter.api.Test; +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonParser; +import tools.jackson.databind.*; +import tools.jackson.databind.deser.DeserializationProblemHandler; +import tools.jackson.databind.exc.InvalidFormatException; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.testutil.DatabindTestUtil; + +import static org.junit.jupiter.api.Assertions.*; + +// For [databind#5469] Add callback to signal null for primitive in DeserializationProblemHandler +public class DeserializationProblemHandler5469Test + extends DatabindTestUtil +{ + + static class Person5469 { + public String id; + public String name; + public long age; + } + + private static int hitCountFirst = 0; + static class ProblemHandler5469 extends DeserializationProblemHandler + { + @Override + public Object handleNullForPrimitives(DeserializationContext ctxt, Class targetType, + ValueDeserializer deser, JsonParser p, String failureMsg + ) throws JacksonException { + hitCountFirst++; + return 5469L; + } + + } + + private static int hitCountSecond = 0; + static class MoreProblemHandler5469 extends DeserializationProblemHandler + { + @Override + public Object handleNullForPrimitives(DeserializationContext ctxt, Class targetType, + ValueDeserializer deser, JsonParser p, String failureMsg + ) throws JacksonException { + hitCountFirst++; + return "THIS IS AN ERROR"; + } + + } + + // SUCCESS Test when problem handler was implemented as required. + @Test + public void testIssue5469HappyCase() + throws Exception + { + // Given + assertEquals(0, hitCountFirst); + ObjectMapper mapper = JsonMapper.builder() + .enable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES) + .addHandler(new ProblemHandler5469()) + .build(); + + // When + Person5469 person = mapper.readValue( + "{\"id\": \"12ab\", \"name\": \"Bob\", " + + // Input is NULL, but.... + "\"age\": null}", Person5469.class); + + // Then + assertNotNull(person); + assertEquals("12ab", person.id); + assertEquals("Bob", person.name); + // We get the MAGIC NUMBER as age + assertEquals(5469L, person.age); + // Sanity check, we hit the code path as we wanted + assertEquals(1, hitCountFirst); + } + + // FAIL! Test when problem handler was implemented WRONG + @Test + public void testIssue5469BadImpl() + throws Exception + { + // Given + assertEquals(0, hitCountFirst); + ObjectMapper mapper = JsonMapper.builder() + .enable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES) + .addHandler(new MoreProblemHandler5469()) + .build(); + + // When + try { + mapper.readValue("{\"id\": \"12ab\", \"name\": \"Bob\", " + + // Input is NULL, to cause problme + "\"age\": null}", Person5469.class); + fail("Should not reach here."); + } catch (InvalidFormatException e) { + // Then + verifyException(e, "Cannot deserialize value of type long from type `java.lang.String`"); + } + } +}