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}.
+ *
+ * - Indicate it does not know what to do by returning {@link #NOT_HANDLED}
+ *
+ * - Throw a {@link JacksonException} to indicate specific fail message (instead of
+ * standard exception caller would throw
+ *
+ * - Handle content to match (by consuming or skipping it), and return actual
+ * instantiated value (of type
targetType) to use as replacement;
+ * value may be `null` as well as expected target type.
+ *
+ *
+ *
+ * @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`");
+ }
+ }
+}