Skip to content

Commit 67f8ef7

Browse files
authored
Fix #5350: add DeserializationFeature.USE_NULL_FOR_MISSING_REFERENCE_VALUES (#5365)
1 parent 8c206b0 commit 67f8ef7

File tree

6 files changed

+69
-15
lines changed

6 files changed

+69
-15
lines changed

release-notes/VERSION

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ Versions: 3.x (for earlier see VERSION-2.x)
55
=== Releases ===
66
------------------------------------------------------------------------
77

8+
3.1.0 (not yet released)
9+
10+
#5350: Add `DeserializationFeature.USE_NULL_FOR_MISSING_REFERENCE_VALUES` for
11+
selecting `null` vs "empty/absent" value when deserializing missing `Optional` value
12+
813
3.0.1 (21-Oct-2025)
914

1015
#5335: JsonMapper to deserialize `Optional.empty()` instead of `null`

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,24 @@ public enum DeserializationFeature implements ConfigFeature
9898
*/
9999
USE_JAVA_ARRAY_FOR_JSON_ARRAY(false),
100100

101+
/**
102+
* Feature that determines whether deserialization of "Reference Types"
103+
* (such as {@link java.util.Optional}, {@link java.util.concurrent.atomic.AtomicReference},
104+
* and Kotlin/Scala equivalents) should return Java {@code null} in case
105+
* of value missing from incoming JSON. If disabled, reference type's
106+
* "absent" value is returned (for example, {@link java.util.Optional#empty()}.
107+
*<p>
108+
* NOTE: this feature only affects handling of missing values; not explicit
109+
* JSON {@code null}s.
110+
* Also note that this feature only affects deserialization when reference value
111+
* is passed via Creator (constructor or factory method) parameter; when
112+
* Setter methods or fields are used, the reference type is left un-assigned
113+
* (this is not specifically related to reference types, but general behavior).
114+
*
115+
* @since 3.1
116+
*/
117+
USE_NULL_FOR_MISSING_REFERENCE_VALUES(false),
118+
101119
/*
102120
/**********************************************************************
103121
/* Error handling features

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

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,9 @@ public AtomicReference<Object> getNullValue(DeserializationContext ctxt) {
3434
return new AtomicReference<>(_valueDeserializer.getNullValue(ctxt));
3535
}
3636

37-
@Override
38-
public Object getAbsentValue(DeserializationContext ctxt) {
39-
// 15-Oct-2025, tatu: For [databind#5335] (and earlier [databind#3601])
40-
// base impl changed to return "null value" (which here means "empty" ref).
41-
// But for AtomicReference we will instead return actual null to maintain
42-
// 2.x compatibility. (3.1 may add configurability for this)
43-
return null;
44-
}
37+
// 25-Oct-2025, tatu: As per [databind#5335], no need to override any more
38+
//@Override
39+
// public Object getAbsentValue(DeserializationContext ctxt) { }
4540

4641
@Override
4742
public AtomicReference<Object> referenceValue(Object contents) {

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,10 +120,14 @@ public Object getEmptyValue(DeserializationContext ctxt) {
120120
public Object getAbsentValue(DeserializationContext ctxt) {
121121
// 21-Sep-2022, tatu: [databind#3601] Let's make absent become `null`,
122122
// NOT "null value" (Empty)
123-
//return null;
123+
// return null;
124124
// 15-Oct-2025, tatu: [databind#5335] Revert above change for 3.0.1 to
125125
// keep compatibility with 2.x series; 3.1 will add configurability
126-
return getNullValue(ctxt);
126+
//return getNullValue(ctxt);
127+
128+
// 25-Oct-2025, tatu: [databind#5350] Now configurable
129+
return ctxt.isEnabled(DeserializationFeature.USE_NULL_FOR_MISSING_REFERENCE_VALUES)
130+
? null : getNullValue(ctxt);
127131
}
128132

129133
public abstract T referenceValue(Object contents);

src/test/java/tools/jackson/databind/deser/jdk/JDKAtomicTypesDeserTest.java

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -400,19 +400,30 @@ public void testAbsentAtomicRefViaCreator() throws Exception
400400
{
401401
AtomicRefBean bean;
402402

403+
ObjectReader r = MAPPER.readerFor(AtomicRefBean.class);
404+
403405
// First: null should become empty, non-null reference
404-
bean = MAPPER.readValue(a2q("{'atomic':null}"), AtomicRefBean.class);
406+
bean = r.readValue(a2q("{'atomic':null}"));
405407
assertNotNull(bean._atomic);
406408
assertNull(bean._atomic.get());
407409

408410
// And then absent (missing), via Creator method, should become actual null
409-
bean = MAPPER.readValue("{}", AtomicRefBean.class);
411+
// 25-Oct-2025, tatu: [databind#53530] Actually, by default should become
412+
// empty ref
413+
bean = r.readValue("{}");
414+
assertNotNull(bean._atomic);
415+
assertNull(bean._atomic.get());
416+
417+
// 25-Oct-2025, tatu: but can reconfigure to get `null` instead
418+
bean = r.with(DeserializationFeature.USE_NULL_FOR_MISSING_REFERENCE_VALUES)
419+
.readValue("{}");
410420
assertNull(bean._atomic);
411421

412-
// Except that we can override handling to produce empty
422+
// Plus can override handling to produce empty
413423
AtomicRefBeanWithEmpty bean2 = MAPPER.readValue("{}", AtomicRefBeanWithEmpty.class);
414424
assertNotNull(bean2._atomic);
415425
assertNull(bean2._atomic.get());
426+
416427
}
417428

418429
// @since 2.14
@@ -429,7 +440,16 @@ public void testAtomicRefWithNodeViaCreator() throws Exception
429440
assertTrue(n.isNull());
430441

431442
// And then absent (missing), via Creator method, should become actual null
443+
// 25-Oct-2025, tatu: [databind#5350] Not any longer... (by default)
432444
bean = MAPPER.readValue("{}", AtomicRefWithNodeBean.class);
445+
assertNotNull(bean._atomicNode);
446+
n = bean._atomicNode.get();
447+
assertTrue(n.isNull());
448+
449+
// but can reconfigure to get `null` instead
450+
bean = MAPPER.readerFor(AtomicRefWithNodeBean.class)
451+
.with(DeserializationFeature.USE_NULL_FOR_MISSING_REFERENCE_VALUES)
452+
.readValue("{}");
433453
assertNull(bean._atomicNode);
434454
}
435455
}

src/test/java/tools/jackson/databind/ext/jdk8/CreatorForOptionalTest.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,10 @@ public CreatorWithOptionalStrings(@JsonProperty("a") Optional<String> a,
4343
@Test
4444
public void testCreatorWithOptional() throws Exception
4545
{
46-
CreatorWithOptionalStrings bean = MAPPER.readValue(
47-
a2q("{'a':'foo'}"), CreatorWithOptionalStrings.class);
46+
final String JSON = a2q("{'a':'foo'}");
47+
ObjectReader r = MAPPER.readerFor(CreatorWithOptionalStrings.class);
48+
49+
CreatorWithOptionalStrings bean = r.readValue(JSON);
4850
assertNotNull(bean);
4951
assertNotNull(bean.a);
5052
assertTrue(bean.a.isPresent());
@@ -55,5 +57,15 @@ public void testCreatorWithOptional() throws Exception
5557

5658
//assertNull(bean.b);
5759
assertEquals(Optional.empty(), bean.b);
60+
61+
// But can be reconfigured
62+
63+
bean = r.with(DeserializationFeature.USE_NULL_FOR_MISSING_REFERENCE_VALUES)
64+
.readValue(JSON);
65+
assertNotNull(bean);
66+
assertNotNull(bean.a);
67+
assertTrue(bean.a.isPresent());
68+
69+
assertNull(bean.b);
5870
}
5971
}

0 commit comments

Comments
 (0)