Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions release-notes/VERSION
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
55 changes: 50 additions & 5 deletions src/main/java/tools/jackson/databind/DeserializationContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -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)
));
Expand Down Expand Up @@ -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)
));
Expand All @@ -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)
));
Expand All @@ -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 <code>msg</code>
*
* @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<DeserializationProblemHandler> 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
Expand All @@ -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 <code>instClass</code>
*/
Expand All @@ -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)
)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
*<ul>
* <li>Indicate it does not know what to do by returning {@link #NOT_HANDLED}
* </li>
* <li>Throw a {@link JacksonException} to indicate specific fail message (instead of
* standard exception caller would throw)
* </li>
* <li>Handle content to match (by consuming or skipping it), and return actual
* instantiated value (of type {@code targetType}) to use as replacement;
* (of expected target type). NOTE: handler <b>SHOULD NOT</b> return {@code null}.
* </li>
* </ul>
*
* @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
* <code>null</code>
*/
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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
}
Expand All @@ -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();
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
}
Expand Down
Loading