Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
35 changes: 35 additions & 0 deletions src/main/java/tools/jackson/databind/DeserializationContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -1354,6 +1354,41 @@ 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 <code>msg</code>
*/
public Object handleNullForPrimitives(Class<?> targetClass,
ValueDeserializer<?> deser, String msg)
throws JacksonException

{
// but if not handled, just throw exception
LinkedNode<DeserializationProblemHandler> h = _config.getProblemHandlers();
while (h != null) {
// Can bail out if it's handled
Object instance = h.value().handleNullForPrimitives(this, deser, msg);
if (instance != DeserializationProblemHandler.NOT_HANDLED) {
// Sanity check for broken handlers, otherwise nasty to debug:
if (_isCompatible(targetClass, instance)) {
return instance;
}
return reportInputMismatch(deser, msg);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is problem with handler providing incompatible value, needs to use specific failure message and NOT generic "no nulls for primitives".

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modified exception thrown AND added test about this case also

}
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -221,6 +222,38 @@ 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}.
*<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</code>) to use as replacement;
* value may be `null` as well as expected target type.
* </li>
* </ul>
*
* @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
* <code>null</code>
*/
public Object handleNullForPrimitives(DeserializationContext ctxt,
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(), 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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()));
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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.core.JsonToken;
import tools.jackson.databind.*;
import tools.jackson.databind.deser.DeserializationProblemHandler;
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.databind.testutil.DatabindTestUtil;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

// For [databind#5469] Add callback to signal null for primitive in DeserializationProblemHandler
public class DeserializationProblemHandler5469Test
extends DatabindTestUtil
{
private static int hitCount = 0;
static class Person5469 {
public String id;
public String name;
public long age;
}

static class ProblemHandler5469 extends DeserializationProblemHandler
{
@Override
public Object handleNullForPrimitives(DeserializationContext ctxt, ValueDeserializer<?> deser, String failureMsg) throws JacksonException {
hitCount++;
return 5469L;
}
}

@Test
public void testIssue5469()
throws Exception
{
// Given
assertEquals(0, hitCount);
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, hitCount);
}
}