Skip to content

Commit 0f7137e

Browse files
committed
fix #5469
1 parent b8bacc2 commit 0f7137e

File tree

5 files changed

+139
-4
lines changed

5 files changed

+139
-4
lines changed

src/main/java/tools/jackson/databind/DeserializationContext.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1354,6 +1354,45 @@ public Object handleWeirdStringValue(Class<?> targetClass, String value,
13541354
throw weirdStringException(value, targetClass, msg);
13551355
}
13561356

1357+
/**
1358+
* Method that deserializers should call if they encounter a null value and
1359+
* target value type is a Primitive type.
1360+
*
1361+
* Default implementation will try to call {@link DeserializationContext#reportInputMismatch(Class, String, Object...)},
1362+
* which by default would throw {@link MismatchedInputException}
1363+
*
1364+
* @param targetClass Type of property into which incoming String should be converted
1365+
* @param deser Type of {@link ValueDeserializer} calling this method.
1366+
* @param msg Error message template caller wants to use if exception is to be thrown
1367+
* @param msgArgs Optional arguments to use for message, if any
1368+
*
1369+
* @throws JacksonException To indicate unrecoverable problem, usually based on <code>msg</code>
1370+
*/
1371+
public Object handleNullForPrimitives(Class<?> targetClass,
1372+
ValueDeserializer<?> deser, String msg, Object... msgArgs)
1373+
throws JacksonException
1374+
1375+
{
1376+
// but if not handled, just throw exception
1377+
msg = _format(msg, msgArgs);
1378+
LinkedNode<DeserializationProblemHandler> h = _config.getProblemHandlers();
1379+
while (h != null) {
1380+
// Can bail out if it's handled
1381+
Object instance = h.value().handleNullForPrimitives(this, deser, msg);
1382+
if (instance != DeserializationProblemHandler.NOT_HANDLED) {
1383+
// Sanity check for broken handlers, otherwise nasty to debug:
1384+
if (_isCompatible(targetClass, instance)) {
1385+
return instance;
1386+
}
1387+
return reportInputMismatch(deser,
1388+
"Cannot map `null` into type %s (set DeserializationConfig.DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES to 'false' to allow)");
1389+
}
1390+
h = h.next();
1391+
}
1392+
return reportInputMismatch(deser,
1393+
"Cannot map `null` into type %s (set DeserializationConfig.DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES to 'false' to allow)");
1394+
}
1395+
13571396
/**
13581397
* Method that deserializers should call if they encounter a numeric value
13591398
* that cannot be converted to target property type, in cases where some

src/main/java/tools/jackson/databind/deser/DeserializationProblemHandler.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import tools.jackson.core.JsonToken;
88
import tools.jackson.databind.*;
99
import tools.jackson.databind.jsontype.TypeIdResolver;
10+
import tools.jackson.databind.util.ClassUtil;
1011

1112
/**
1213
* This is the class that can be registered (via
@@ -221,6 +222,38 @@ public Object handleUnexpectedToken(DeserializationContext ctxt,
221222
return NOT_HANDLED;
222223
}
223224

225+
/**
226+
* Method that deserializers should call if the {@code null} value is encountered and
227+
* need to deserialize as a Primitive types (e.g. int, long etc...) that is, type of token that deserializer
228+
* cannot handle). This could occur, for example, if a Number deserializer
229+
* encounter {@link JsonToken#START_ARRAY} instead of
230+
* {@link JsonToken#VALUE_NUMBER_INT} or {@link JsonToken#VALUE_NUMBER_FLOAT}.
231+
*<ul>
232+
* <li>Indicate it does not know what to do by returning {@link #NOT_HANDLED}
233+
* </li>
234+
* <li>Throw a {@link JacksonException} to indicate specific fail message (instead of
235+
* standard exception caller would throw
236+
* </li>
237+
* <li>Handle content to match (by consuming or skipping it), and return actual
238+
* instantiated value (of type <code>targetType</code>) to use as replacement;
239+
* value may be `null` as well as expected target type.
240+
* </li>
241+
* </ul>
242+
*
243+
* @param failureMsg Message that will be used by caller
244+
* to indicate type of failure unless handler produces value to use
245+
*
246+
* @return Either {@link #NOT_HANDLED} to indicate that handler does not know
247+
* what to do (and exception may be thrown), or value to use (possibly
248+
* <code>null</code>
249+
*/
250+
public Object handleNullForPrimitives(DeserializationContext ctxt,
251+
ValueDeserializer<?> deser, String failureMsg)
252+
throws JacksonException
253+
{
254+
return NOT_HANDLED;
255+
}
256+
224257
/**
225258
* Method called when instance creation for a type fails due to an exception.
226259
* Handler may choose to do one of following things:

src/main/java/tools/jackson/databind/deser/jdk/NumberDeserializers.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,13 +159,14 @@ public AccessPattern getNullAccessPattern() {
159159
return AccessPattern.CONSTANT;
160160
}
161161

162+
@SuppressWarnings("unchecked")
162163
@Override
163164
public final T getNullValue(DeserializationContext ctxt) {
164165
// 01-Mar-2017, tatu: Alas, not all paths lead to `_coerceNull()`, as `SettableBeanProperty`
165166
// short-circuits `null` handling. Hence need this check as well.
166167
if (_primitive && ctxt.isEnabled(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES)) {
167-
ctxt.reportInputMismatch(this,
168-
"Cannot map `null` into type %s (set DeserializationConfig.DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES to 'false' to allow)",
168+
return (T) ctxt.handleNullForPrimitives(handledType(), this, "Cannot map `null` into type %s (set DeserializationConfig.DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES to 'false' to allow)",
169+
// Fix
169170
ClassUtil.classNameOf(handledType()));
170171
}
171172
return _nullValue;

src/main/java/tools/jackson/databind/deser/std/StdDeserializer.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1638,8 +1638,9 @@ protected final void _verifyNullForPrimitive(DeserializationContext ctxt)
16381638
throws DatabindException
16391639
{
16401640
if (ctxt.isEnabled(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES)) {
1641-
ctxt.reportInputMismatch(this,
1642-
"Cannot coerce `null` to %s (disable `DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES` to allow)",
1641+
ctxt.handleNullForPrimitives(handledType(),
1642+
this,
1643+
"Cannot coerce `null` to %s (disable `DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES` to allow)",
16431644
_coercedTypeDesc());
16441645
}
16451646
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package tools.jackson.databind.deser.filter;
2+
3+
import org.junit.jupiter.api.Test;
4+
import tools.jackson.core.JacksonException;
5+
import tools.jackson.core.JsonParser;
6+
import tools.jackson.core.JsonToken;
7+
import tools.jackson.databind.*;
8+
import tools.jackson.databind.deser.DeserializationProblemHandler;
9+
import tools.jackson.databind.json.JsonMapper;
10+
import tools.jackson.databind.testutil.DatabindTestUtil;
11+
12+
import static org.junit.jupiter.api.Assertions.assertEquals;
13+
import static org.junit.jupiter.api.Assertions.assertNotNull;
14+
15+
// For [databind#5469] Add callback to signal null for primitive in DeserializationProblemHandler
16+
public class DeserializationProblemHandler5469Test
17+
extends DatabindTestUtil
18+
{
19+
private static int hitCount = 0;
20+
static class Person5469 {
21+
public String id;
22+
public String name;
23+
public long age;
24+
}
25+
26+
static class ProblemHandler5469 extends DeserializationProblemHandler
27+
{
28+
@Override
29+
public Object handleNullForPrimitives(DeserializationContext ctxt, ValueDeserializer<?> deser, String failureMsg) throws JacksonException {
30+
hitCount++;
31+
return 5469L;
32+
}
33+
}
34+
35+
@Test
36+
public void testIssue5469()
37+
throws Exception
38+
{
39+
// Given
40+
assertEquals(0, hitCount);
41+
ObjectMapper mapper = JsonMapper.builder()
42+
.enable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES)
43+
.addHandler(new ProblemHandler5469())
44+
.build();
45+
46+
// When
47+
Person5469 person = mapper.readValue(
48+
"{\"id\": \"12ab\", \"name\": \"Bob\", " +
49+
// Input is NULL, but....
50+
"\"age\": null}", Person5469.class);
51+
52+
// Then
53+
assertNotNull(person);
54+
assertEquals("12ab", person.id);
55+
assertEquals("Bob", person.name);
56+
// We get the MAGIC NUMBER as age
57+
assertEquals(5469L, person.age);
58+
// Sanity check, we hit the code path as we wanted
59+
assertEquals(1, hitCount);
60+
}
61+
}

0 commit comments

Comments
 (0)