diff --git a/release-notes/VERSION b/release-notes/VERSION index a8810f2faf..fd4597f0c5 100644 --- a/release-notes/VERSION +++ b/release-notes/VERSION @@ -49,6 +49,10 @@ Versions: 3.x (for earlier see VERSION-2.x) #5456: Additional configuration (`JsonNodeFeature.STRIP_TRAILING_BIGDECIMAL_ZEROES`: true) to MapperBuilder#configureForJackson2 to closer match Jackson 2 behavior (contributed by @nrayburn-tech) +#5469 : Add callback in `DeserializationProblemHandler` for "null for primitive" + problem case + (reported by @jnizet) + (fixed by Joo-Hyuk K) #5475: Support `@JsonDeserializeAs` annotation (implemented by @cowtowncoder, w/ Claude code) diff --git a/src/main/java/tools/jackson/databind/DeserializationContext.java b/src/main/java/tools/jackson/databind/DeserializationContext.java index 65f7d8b176..c30afb11d5 100644 --- a/src/main/java/tools/jackson/databind/DeserializationContext.java +++ b/src/main/java/tools/jackson/databind/DeserializationContext.java @@ -1344,7 +1344,7 @@ public Object handleWeirdStringValue(Class targetClass, String value, return instance; } throw weirdStringException(value, targetClass, String.format( - "DeserializationProblemHandler.handleWeirdStringValue() for type %s returned value of type %s", +"`DeserializationProblemHandler.handleWeirdStringValue()` for type %s returned value of type %s", ClassUtil.getClassDescription(targetClass), ClassUtil.getClassDescription(instance) )); @@ -1387,7 +1387,7 @@ public Object handleWeirdNumberValue(Class targetClass, Number value, return key; } throw weirdNumberException(value, targetClass, _format( - "DeserializationProblemHandler.handleWeirdNumberValue() for type %s returned value of type %s", +"`DeserializationProblemHandler.handleWeirdNumberValue()` for type %s returned value of type %s", ClassUtil.getClassDescription(targetClass), ClassUtil.getClassDescription(key) )); @@ -1412,7 +1412,7 @@ public Object handleWeirdNativeValue(JavaType targetType, Object badValue, return goodValue; } throw DatabindException.from(p, _format( -"DeserializationProblemHandler.handleWeirdNativeValue() for type %s returned value of type %s", +"`DeserializationProblemHandler.handleWeirdNativeValue()` for type %s returned value of type %s", ClassUtil.getClassDescription(targetType), ClassUtil.getClassDescription(goodValue) )); @@ -1421,6 +1421,51 @@ public Object handleWeirdNativeValue(JavaType targetType, Object badValue, throw weirdNativeValueException(badValue, raw); } + /** + * 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 DeserializationProblemHandler#handleNullForPrimitives} + * on configured handlers, if any, to allow for recovery; if recovery does not + * succeed, will call {@link #reportInputMismatch} with given message, + * which will throw {@link MismatchedInputException}. + * + * @param targetClass Primitive type into which incoming {@code null} value should be converted to + * @param p Parser that points to the {@code null} read + * @param deser Type of {@link ValueDeserializer} calling this method + * @param msgTemplate Error message template caller wants to use if exception is to be thrown + * @param msgArgs Arguments for {@code msgTemplate} (if any) + * + * @throws JacksonException To indicate unrecoverable problem, usually based on msg + * + * @since 3.1 + */ + public Object handleNullForPrimitives(Class targetClass, + JsonParser p, ValueDeserializer deser, + String msgTemplate, Object... msgArgs) + throws JacksonException + { + // but if not handled, just throw exception + LinkedNode h = _config.getProblemHandlers(); + String msg = _format(msgTemplate, msgArgs); + while (h != null) { + // Can bail out if it's handled + Object instance = h.value().handleNullForPrimitives(this, targetClass, p, deser, 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("`DeserializationProblemHandler.handleNullForPrimitives()` for type %s returned value of type %s", + ClassUtil.nameOf(targetClass), ClassUtil.getClassDescription(instance)), + instance, targetClass + ); + } + h = h.next(); + } + return reportInputMismatch(deser, msg); + } /** * Method that deserializers should call if they fail to instantiate value * due to lack of viable instantiator (usually creator, that is, constructor @@ -1432,7 +1477,7 @@ public Object handleWeirdNativeValue(JavaType targetType, Object badValue, * @param instClass Type that was to be instantiated * @param valueInst (optional) Value instantiator to be used, if any; null if type does not * use one for instantiation (custom deserialiers don't; standard POJO deserializer does) - * @param p Parser that points to the JSON value to decode + * @param p Parser that points to the input value to decode * * @return Object that should be constructed, if any; has to be of type instClass */ @@ -1456,7 +1501,7 @@ public Object handleMissingInstantiator(Class instClass, ValueInstantiator va return instance; } reportBadDefinition(constructType(instClass), String.format( -"DeserializationProblemHandler.handleMissingInstantiator() for type %s returned value of type %s", +"`DeserializationProblemHandler.handleMissingInstantiator()` for type %s returned value of type %s", ClassUtil.getClassDescription(instClass), ClassUtil.getClassDescription((instance) ))); diff --git a/src/main/java/tools/jackson/databind/deser/DeserializationProblemHandler.java b/src/main/java/tools/jackson/databind/deser/DeserializationProblemHandler.java index 8cd736b745..ee33a7181b 100644 --- a/src/main/java/tools/jackson/databind/deser/DeserializationProblemHandler.java +++ b/src/main/java/tools/jackson/databind/deser/DeserializationProblemHandler.java @@ -221,6 +221,42 @@ public Object handleUnexpectedToken(DeserializationContext ctxt, return NOT_HANDLED; } + /** + * Method that deserializers should call if the {@code null} value is encountered when + * deserializing a Java primitive types ({@code int}, {@code long} etc) and + * {@link DeserializationFeature#FAIL_ON_NULL_FOR_PRIMITIVES} is enabled. + * Handler needs to do one of: + * + * + * @param ctxt Deserialization context + * @param targetType Target type to deserialize into + * @param p JsonParser used to read {@code null} input token + * @param deser Target deserializer that attempted to deserialize {@code null} target value + * @param failureMsg Message that will be used by caller to indicate type of failure unless + * handler produces a 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, JsonParser p, ValueDeserializer deser, 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..e0bc206d07 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,15 @@ 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(), ctxt.getParser(), this, +"Cannot map `null` into type %s (set `DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES` to 'false' to allow)", + ClassUtil.nameOf(handledType())); } return _nullValue; } @@ -476,7 +477,8 @@ public Character deserialize(JsonParser p, DeserializationContext ctxt) "value outside valid Character range (0x0000 - 0xFFFF)"); case JsonTokenId.ID_NULL: if (_primitive) { - _verifyNullForPrimitive(ctxt); + char c = (char) _verifyNullForPrimitive(ctxt, p, '\0'); + return c; } return (Character) getNullValue(ctxt); case JsonTokenId.ID_START_ARRAY: diff --git a/src/main/java/tools/jackson/databind/deser/jdk/PrimitiveArrayDeserializers.java b/src/main/java/tools/jackson/databind/deser/jdk/PrimitiveArrayDeserializers.java index 7603bb662f..4aeeed10f0 100644 --- a/src/main/java/tools/jackson/databind/deser/jdk/PrimitiveArrayDeserializers.java +++ b/src/main/java/tools/jackson/databind/deser/jdk/PrimitiveArrayDeserializers.java @@ -282,8 +282,9 @@ public char[] deserialize(JsonParser p, DeserializationContext ctxt) throws Jack _nuller.getNullValue(ctxt); continue; } - _verifyNullForPrimitive(ctxt); - str = "\0"; + char value = (char) _verifyNullForPrimitive(ctxt, p, '\0'); + sb.append(value); + continue; } else { CharSequence cs = (CharSequence) ctxt.handleUnexpectedToken(getValueType(ctxt), p); str = cs.toString(); @@ -382,8 +383,7 @@ public boolean[] deserialize(JsonParser p, DeserializationContext ctxt) _nuller.getNullValue(ctxt); continue; } - _verifyNullForPrimitive(ctxt); - value = false; + value = (boolean) _verifyNullForPrimitive(ctxt, p, false); } else { value = _parseBooleanPrimitive(p, ctxt); } @@ -497,8 +497,7 @@ public byte[] deserialize(JsonParser p, DeserializationContext ctxt) throws Jack _nuller.getNullValue(ctxt); continue; } - _verifyNullForPrimitive(ctxt); - value = (byte) 0; + value = (byte) _verifyNullForPrimitive(ctxt, p, (byte) 0); } else { value = _parseBytePrimitive(p, ctxt); } @@ -525,14 +524,13 @@ protected byte[] handleSingleElementUnwrapped(JsonParser p, if (t == JsonToken.VALUE_NUMBER_INT) { value = p.getByteValue(); // note: may throw due to overflow } else { - // should probably accept nulls as 'false' + // should we accept nulls as null for byte[], or { 0 } ? if (t == JsonToken.VALUE_NULL) { if (_nuller != null) { _nuller.getNullValue(ctxt); return (byte[]) getEmptyValue(ctxt); } - _verifyNullForPrimitive(ctxt); - return null; + value = (byte) _verifyNullForPrimitive(ctxt, p, (byte) 0); } Number n = (Number) ctxt.handleUnexpectedToken(getValueType(ctxt), p); value = n.byteValue(); @@ -589,8 +587,7 @@ public short[] deserialize(JsonParser p, DeserializationContext ctxt) throws Jac _nuller.getNullValue(ctxt); continue; } - _verifyNullForPrimitive(ctxt); - value = (short) 0; + value = (short) _verifyNullForPrimitive(ctxt, p, (short) 0); } else { value = _parseShortPrimitive(p, ctxt); } @@ -666,8 +663,7 @@ public int[] deserialize(JsonParser p, DeserializationContext ctxt) throws Jacks _nuller.getNullValue(ctxt); continue; } - _verifyNullForPrimitive(ctxt); - value = 0; + value = (int) _verifyNullForPrimitive(ctxt, p, 0); } else { value = _parseIntPrimitive(p, ctxt); } @@ -743,8 +739,7 @@ public long[] deserialize(JsonParser p, DeserializationContext ctxt) throws Jack _nuller.getNullValue(ctxt); continue; } - _verifyNullForPrimitive(ctxt); - value = 0L; + value = (long) _verifyNullForPrimitive(ctxt, p, 0L); } else { value = _parseLongPrimitive(p, ctxt); } 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 4ac56aecea..12cd3809ff 100644 --- a/src/main/java/tools/jackson/databind/deser/std/StdDeserializer.java +++ b/src/main/java/tools/jackson/databind/deser/std/StdDeserializer.java @@ -348,10 +348,10 @@ protected final boolean _parseBooleanPrimitive(JsonParser p, DeserializationCont return true; case JsonTokenId.ID_FALSE: return false; - case JsonTokenId.ID_NULL: // null fine for non-primitive - _verifyNullForPrimitive(ctxt); - return false; - // 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML) + case JsonTokenId.ID_NULL: // null may or may be ok for primitive + return (boolean) _verifyNullForPrimitive(ctxt, p, false); + + // 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML) case JsonTokenId.ID_START_OBJECT: text = ctxt.extractScalarFromObject(p, this, Boolean.TYPE); // 17-May-2025, tatu: [databind#4656] need to check for `null` @@ -377,8 +377,7 @@ protected final boolean _parseBooleanPrimitive(JsonParser p, DeserializationCont final CoercionAction act = _checkFromStringCoercion(ctxt, text, LogicalType.Boolean, Boolean.TYPE); if (act == CoercionAction.AsNull) { - _verifyNullForPrimitive(ctxt); - return false; + return (boolean) _verifyNullForPrimitive(ctxt, p, false); } if (act == CoercionAction.AsEmpty) { return false; @@ -535,8 +534,7 @@ protected final byte _parseBytePrimitive(JsonParser p, DeserializationContext ct case JsonTokenId.ID_NUMBER_INT: return p.getByteValue(); case JsonTokenId.ID_NULL: - _verifyNullForPrimitive(ctxt); - return (byte) 0; + return (byte) _verifyNullForPrimitive(ctxt, p, (byte) 0); // 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML) case JsonTokenId.ID_START_OBJECT: text = ctxt.extractScalarFromObject(p, this, Byte.TYPE); @@ -565,8 +563,7 @@ protected final byte _parseBytePrimitive(JsonParser p, DeserializationContext ct LogicalType.Integer, Byte.TYPE); if (act == CoercionAction.AsNull) { // 03-May-2021, tatu: Might not be allowed (should we do "empty" check?) - _verifyNullForPrimitive(ctxt); - return (byte) 0; + return (byte) _verifyNullForPrimitive(ctxt, p, (byte) 0); } if (act == CoercionAction.AsEmpty) { return (byte) 0; @@ -619,8 +616,7 @@ protected final short _parseShortPrimitive(JsonParser p, DeserializationContext case JsonTokenId.ID_NUMBER_INT: return p.getShortValue(); case JsonTokenId.ID_NULL: - _verifyNullForPrimitive(ctxt); - return (short) 0; + return (short) _verifyNullForPrimitive(ctxt, p, (short) 0); // 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML) case JsonTokenId.ID_START_OBJECT: text = ctxt.extractScalarFromObject(p, this, Short.TYPE); @@ -648,8 +644,7 @@ protected final short _parseShortPrimitive(JsonParser p, DeserializationContext LogicalType.Integer, Short.TYPE); if (act == CoercionAction.AsNull) { // 03-May-2021, tatu: Might not be allowed (should we do "empty" check?) - _verifyNullForPrimitive(ctxt); - return (short) 0; + return (short) _verifyNullForPrimitive(ctxt, p, (short) 0); } if (act == CoercionAction.AsEmpty) { return (short) 0; @@ -696,8 +691,7 @@ protected int _parseIntPrimitive(JsonParser p, DeserializationContext ctxt) // Here regular (strict) accessor is fine return p.getIntValue(); case JsonTokenId.ID_NULL: - _verifyNullForPrimitive(ctxt); - return 0; + return (int) _verifyNullForPrimitive(ctxt, p, 0); // 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML) case JsonTokenId.ID_START_OBJECT: text = ctxt.extractScalarFromObject(p, this, Integer.TYPE); @@ -724,8 +718,7 @@ protected int _parseIntPrimitive(JsonParser p, DeserializationContext ctxt) LogicalType.Integer, Integer.TYPE); if (act == CoercionAction.AsNull) { // 03-May-2021, tatu: Might not be allowed (should we do "empty" check?) - _verifyNullForPrimitive(ctxt); - return 0; + return (int) _verifyNullForPrimitive(ctxt, p, 0); } if (act == CoercionAction.AsEmpty) { return 0; @@ -852,8 +845,7 @@ protected final long _parseLongPrimitive(JsonParser p, DeserializationContext ct case JsonTokenId.ID_NUMBER_INT: return p.getLongValue(); case JsonTokenId.ID_NULL: - _verifyNullForPrimitive(ctxt); - return 0L; + return (long) _verifyNullForPrimitive(ctxt, p, 0L); // 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML) case JsonTokenId.ID_START_OBJECT: text = ctxt.extractScalarFromObject(p, this, Long.TYPE); @@ -880,8 +872,7 @@ protected final long _parseLongPrimitive(JsonParser p, DeserializationContext ct LogicalType.Integer, Long.TYPE); if (act == CoercionAction.AsNull) { // 03-May-2021, tatu: Might not be allowed (should we do "empty" check?) - _verifyNullForPrimitive(ctxt); - return 0L; + return (long) _verifyNullForPrimitive(ctxt, p, 0L); } if (act == CoercionAction.AsEmpty) { return 0L; @@ -992,8 +983,7 @@ protected final float _parseFloatPrimitive(JsonParser p, DeserializationContext case JsonTokenId.ID_NUMBER_FLOAT: return p.getFloatValue(); case JsonTokenId.ID_NULL: - _verifyNullForPrimitive(ctxt); - return 0f; + return (float) _verifyNullForPrimitive(ctxt, p, 0f); // 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML) case JsonTokenId.ID_START_OBJECT: text = ctxt.extractScalarFromObject(p, this, Float.TYPE); @@ -1030,8 +1020,7 @@ protected final float _parseFloatPrimitive(JsonParser p, DeserializationContext LogicalType.Integer, Float.TYPE); if (act == CoercionAction.AsNull) { // 03-May-2021, tatu: Might not be allowed (should we do "empty" check?) - _verifyNullForPrimitive(ctxt); - return 0.0f; + return (float) _verifyNullForPrimitive(ctxt, p, 0.0f); } if (act == CoercionAction.AsEmpty) { return 0.0f; @@ -1112,8 +1101,7 @@ protected final double _parseDoublePrimitive(JsonParser p, DeserializationContex case JsonTokenId.ID_NUMBER_FLOAT: return p.getDoubleValue(); case JsonTokenId.ID_NULL: - _verifyNullForPrimitive(ctxt); - return 0.0; + return (double)_verifyNullForPrimitive(ctxt, p, 0.0); // 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML) case JsonTokenId.ID_START_OBJECT: text = ctxt.extractScalarFromObject(p, this, Double.TYPE); @@ -1150,8 +1138,7 @@ protected final double _parseDoublePrimitive(JsonParser p, DeserializationContex LogicalType.Integer, Double.TYPE); if (act == CoercionAction.AsNull) { // 03-May-2021, tatu: Might not be allowed (should we do "empty" check?) - _verifyNullForPrimitive(ctxt); - return 0.0; + return (double)_verifyNullForPrimitive(ctxt, p, 0.0); } if (act == CoercionAction.AsEmpty) { return 0.0; @@ -1634,16 +1621,29 @@ protected Object _coerceIntegral(JsonParser p, DeserializationContext ctxt) thro * was received by other means (coerced due to configuration, or even from * optionally acceptable String {@code "null"} token). */ - protected final void _verifyNullForPrimitive(DeserializationContext ctxt) + protected Object _verifyNullForPrimitive(DeserializationContext ctxt, + JsonParser p, Object defaultValue) throws DatabindException { if (ctxt.isEnabled(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES)) { - ctxt.reportInputMismatch(this, + return ctxt.handleNullForPrimitives(handledType(), p, + this, "Cannot coerce `null` to %s (disable `DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES` to allow)", - _coercedTypeDesc()); + _coercedTypeDesc()); } + return defaultValue; } + /** + * @deprecated Since 3.1 use overload {@link #_verifyNullForPrimitive(DeserializationContext, JsonParser, Object)} + */ + @Deprecated // @since 3.1 + protected final void _verifyNullForPrimitive(DeserializationContext ctxt) + throws DatabindException + { + _verifyNullForPrimitive(ctxt, ctxt.getParser(), null); + } + /** * Method called to verify that text value {@code "null"} from input is acceptable * for primitive (unboxed) target type. It should not be called if actual 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..f69b53bf7e --- /dev/null +++ b/src/test/java/tools/jackson/databind/deser/filter/DeserializationProblemHandler5469Test.java @@ -0,0 +1,103 @@ +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, + JsonParser p, ValueDeserializer deser, 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, + JsonParser p, ValueDeserializer deser, String failureMsg + ) throws JacksonException { + hitCountSecond++; + 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, hitCountSecond); + 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 problem + "\"age\": null}", Person5469.class); + // Sanity check, we hit the code path as we wanted + assertEquals(1, hitCountSecond); + fail("Should not reach here."); + } catch (InvalidFormatException e) { + // Then + verifyException(e, + "`DeserializationProblemHandler.handleNullForPrimitives()` for type `long` returned value of type `java.lang.String`"); + } + } +}