Skip to content

Commit 5700ac2

Browse files
cowtowncoderclaude
andcommitted
Merge branch '2.x' into 3.1
Port #6338 fix (honor `Nulls.SKIP` for creator properties in `readerForUpdating()`) to `tools.jackson` and add 3.1.9 release notes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 parents a9187b1 + 071a1e2 commit 5700ac2

6 files changed

Lines changed: 207 additions & 4 deletions

File tree

‎release-notes/CREDITS‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,9 @@ Martin Mirchev (@Marti2203)
480480
* Reported, fixed #6339: Enum aliases from an appended `AnnotationIntrospector` are
481481
discarded: `JacksonAnnotationIntrospector.findEnumAliases()` overwrites every slot
482482
[3.1.9]
483+
* Reported, fixed #6338: `readerForUpdating()` ignores `@JsonSetter(nulls = Nulls.SKIP)`
484+
for creator properties: existing value is overwritten with `null`
485+
[3.1.9]
483486

484487
Anna Breeva (@ab310013)
485488
* Reported #6315: Map key deserializers should apply same type checks as bean

‎release-notes/CREDITS-2.x‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2142,6 +2142,9 @@ Martin Mirchev (@Marti2203)
21422142
* Reported, fixed #6306: NPE serializing `@JsonIdentityReference(alwaysAsId = true)`
21432143
to an object whose id is `null`
21442144
[2.23.0]
2145+
* Reported, fixed #6338: `readerForUpdating()` ignores `@JsonSetter(nulls = Nulls.SKIP)`
2146+
for creator properties: existing value is overwritten with `null`
2147+
[2.23.0]
21452148
* Reported, fixed #6339: Enum aliases from an appended `AnnotationIntrospector` are
21462149
discarded: `JacksonAnnotationIntrospector.findEnumAliases()` overwrites every slot
21472150
[2.23.0]

‎release-notes/VERSION‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ No changes since 3.1
3636
#6329: Add JDK-bundled `RowSet` gadget siblings of `JdbcRowSetImpl` to
3737
deserialization deny-list (`CachedRowSetImpl`, `WebRowSetImpl`,
3838
`FilteredRowSetImpl`, `JoinRowSetImpl`)
39+
#6338: `readerForUpdating()` ignores `@JsonSetter(nulls = Nulls.SKIP)` for creator
40+
properties: existing value is overwritten with `null`
41+
(reported, fixed by @Marti2203)
3942
#6339: Enum aliases from an appended `AnnotationIntrospector` are discarded:
4043
`JacksonAnnotationIntrospector.findEnumAliases()` overwrites every slot
4144
(reported, fixed by @Marti2203)

‎release-notes/VERSION-2.x‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ Project: jackson-databind
3535
#6329: Add JDK-bundled `RowSet` gadget siblings of `JdbcRowSetImpl` to
3636
deserialization deny-list (`CachedRowSetImpl`, `WebRowSetImpl`,
3737
`FilteredRowSetImpl`, `JoinRowSetImpl`)
38+
#6338: `readerForUpdating()` ignores `@JsonSetter(nulls = Nulls.SKIP)` for creator
39+
properties: existing value is overwritten with `null`
40+
(reported, fixed by @Marti2203)
3841
#6339: Enum aliases from an appended `AnnotationIntrospector` are discarded:
3942
`JacksonAnnotationIntrospector.findEnumAliases()` overwrites every slot
4043
(reported, fixed by @Marti2203)

‎src/main/java/tools/jackson/databind/deser/CreatorProperty.java‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import tools.jackson.core.JacksonException;
88
import tools.jackson.core.JsonParser;
99
import tools.jackson.databind.*;
10+
import tools.jackson.databind.deser.impl.NullsConstantProvider;
1011
import tools.jackson.databind.exc.InvalidDefinitionException;
1112
import tools.jackson.databind.introspect.AnnotatedMember;
1213
import tools.jackson.databind.introspect.AnnotatedParameter;
@@ -218,28 +219,35 @@ public <A extends Annotation> A getAnnotation(Class<A> acls) {
218219
public void deserializeAndSet(JsonParser p, DeserializationContext ctxt,
219220
Object instance) throws JacksonException
220221
{
221-
_verifySetter();
222-
_fallbackSetter.set(ctxt, instance, deserialize(p, ctxt));
222+
set(ctxt, instance, deserialize(p, ctxt));
223223
}
224224

225225
@Override
226226
public Object deserializeSetAndReturn(JsonParser p,
227227
DeserializationContext ctxt, Object instance) throws JacksonException
228228
{
229-
_verifySetter();
230-
return _fallbackSetter.setAndReturn(ctxt, instance, deserialize(p, ctxt));
229+
return setAndReturn(ctxt, instance, deserialize(p, ctxt));
231230
}
232231

233232
@Override
234233
public void set(DeserializationContext ctxt, Object instance, Object value)
235234
{
235+
// [databind#6338]: fallback setter is not contextualized, so must
236+
// check for "skip nulls" here. Note: `deserialize()` returns `null`
237+
// for both JSON null and values coerced to null ([databind#2023])
238+
if ((value == null) && NullsConstantProvider.isSkipper(_nullProvider)) {
239+
return;
240+
}
236241
_verifySetter();
237242
_fallbackSetter.set(ctxt, instance, value);
238243
}
239244

240245
@Override
241246
public Object setAndReturn(DeserializationContext ctxt, Object instance, Object value)
242247
{
248+
if ((value == null) && NullsConstantProvider.isSkipper(_nullProvider)) {
249+
return instance;
250+
}
243251
_verifySetter();
244252
return _fallbackSetter.setAndReturn(ctxt,instance, value);
245253
}
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
package tools.jackson.databind.deser.filter;
2+
3+
import java.util.ArrayList;
4+
import java.util.Arrays;
5+
import java.util.List;
6+
7+
import org.junit.jupiter.api.Test;
8+
9+
import com.fasterxml.jackson.annotation.*;
10+
11+
import tools.jackson.core.JsonParser;
12+
import tools.jackson.core.JsonToken;
13+
14+
import tools.jackson.databind.DeserializationContext;
15+
import tools.jackson.databind.ObjectMapper;
16+
import tools.jackson.databind.PropertyName;
17+
import tools.jackson.databind.cfg.CoercionAction;
18+
import tools.jackson.databind.cfg.CoercionInputShape;
19+
import tools.jackson.databind.deser.CreatorProperty;
20+
import tools.jackson.databind.deser.SettableBeanProperty;
21+
import tools.jackson.databind.deser.bean.BeanDeserializerBase;
22+
import tools.jackson.databind.json.JsonMapper;
23+
import tools.jackson.databind.testutil.DatabindTestUtil;
24+
25+
import static org.junit.jupiter.api.Assertions.assertEquals;
26+
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
27+
import static org.junit.jupiter.api.Assertions.assertNull;
28+
import static org.junit.jupiter.api.Assertions.assertSame;
29+
30+
// [databind#6338]: readerForUpdating() must honor Nulls.SKIP for
31+
// creator properties too
32+
public class SkipNullsCreatorUpdate6338Test
33+
extends DatabindTestUtil
34+
{
35+
static class FieldBean {
36+
@JsonSetter(nulls = Nulls.SKIP)
37+
public List<String> values;
38+
public String name;
39+
40+
@JsonCreator
41+
public FieldBean(@JsonProperty("values") List<String> values) {
42+
this.values = values;
43+
}
44+
}
45+
46+
static class SetterBean {
47+
protected List<String> values;
48+
public String name;
49+
50+
@JsonCreator
51+
public SetterBean(@JsonProperty("values") List<String> values) {
52+
this.values = values;
53+
}
54+
55+
@JsonSetter(nulls = Nulls.SKIP)
56+
public void setValues(List<String> values) {
57+
this.values = values;
58+
}
59+
}
60+
61+
static class PlainBean {
62+
public List<String> values;
63+
public String name;
64+
65+
@JsonCreator
66+
public PlainBean(@JsonProperty("values") List<String> values) {
67+
this.values = values;
68+
}
69+
}
70+
71+
// No setter or field for "value", so no fallback setter either
72+
// (note: not a Collection, to avoid "setterless" property via getter)
73+
static class CreatorOnlyBean {
74+
protected String _value;
75+
public String name;
76+
77+
@JsonCreator
78+
public CreatorOnlyBean(@JsonProperty("value")
79+
@JsonSetter(nulls = Nulls.SKIP) String value) {
80+
_value = value;
81+
}
82+
83+
public String getValue() { return _value; }
84+
}
85+
86+
private final ObjectMapper MAPPER = newJsonMapper();
87+
88+
private final static String JSON = a2q("{'values':null,'name':'x'}");
89+
90+
@Test
91+
public void testSkipNullsForCreatorField() throws Exception
92+
{
93+
FieldBean bean = new FieldBean(new ArrayList<>(Arrays.asList("a")));
94+
MAPPER.readerForUpdating(bean).readValue(JSON);
95+
assertEquals("x", bean.name);
96+
assertEquals(Arrays.asList("a"), bean.values);
97+
}
98+
99+
@Test
100+
public void testSkipNullsForCreatorSetter() throws Exception
101+
{
102+
SetterBean bean = new SetterBean(new ArrayList<>(Arrays.asList("a")));
103+
MAPPER.readerForUpdating(bean).readValue(JSON);
104+
assertEquals("x", bean.name);
105+
assertEquals(Arrays.asList("a"), bean.values);
106+
}
107+
108+
@Test
109+
public void testSkipNullsForCreatorGlobalDefault() throws Exception
110+
{
111+
ObjectMapper mapper = JsonMapper.builder()
112+
.changeDefaultNullHandling(n -> n.withValueNulls(Nulls.SKIP))
113+
.build();
114+
PlainBean bean = new PlainBean(new ArrayList<>(Arrays.asList("a")));
115+
mapper.readerForUpdating(bean).readValue(JSON);
116+
assertEquals("x", bean.name);
117+
assertEquals(Arrays.asList("a"), bean.values);
118+
}
119+
120+
// Without "skip nulls", null is still set as before
121+
@Test
122+
public void testNullSetForCreatorWithoutSkip() throws Exception
123+
{
124+
PlainBean bean = new PlainBean(new ArrayList<>(Arrays.asList("a")));
125+
MAPPER.readerForUpdating(bean).readValue(JSON);
126+
assertEquals("x", bean.name);
127+
assertNull(bean.values);
128+
}
129+
130+
// Values coerced into null (here, empty String) must be skipped as well
131+
@Test
132+
public void testSkipNullsForCreatorCoercedNull() throws Exception
133+
{
134+
ObjectMapper mapper = JsonMapper.builder()
135+
.withCoercionConfigDefaults(cfg ->
136+
cfg.setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsNull))
137+
.build();
138+
FieldBean bean = new FieldBean(new ArrayList<>(Arrays.asList("a")));
139+
mapper.readerForUpdating(bean).readValue(a2q("{'values':'','name':'x'}"));
140+
assertEquals("x", bean.name);
141+
assertEquals(Arrays.asList("a"), bean.values);
142+
}
143+
144+
// Other mutation methods of CreatorProperty must also skip nulls
145+
@Test
146+
public void testSkipNullsForCreatorSetMethods() throws Exception
147+
{
148+
try (JsonParser p = MAPPER.createParser("null")) {
149+
DeserializationContext ctxt = MAPPER._deserializationContext().assignParser(p);
150+
BeanDeserializerBase deser = (BeanDeserializerBase) ctxt.findRootValueDeserializer(
151+
ctxt.constructType(FieldBean.class));
152+
SettableBeanProperty prop = deser.findProperty(PropertyName.construct("values"));
153+
assertInstanceOf(CreatorProperty.class, prop);
154+
155+
List<String> orig = new ArrayList<>(Arrays.asList("a"));
156+
FieldBean bean = new FieldBean(orig);
157+
158+
assertEquals(JsonToken.VALUE_NULL, p.nextToken());
159+
assertSame(bean, prop.deserializeSetAndReturn(p, ctxt, bean));
160+
assertSame(orig, bean.values);
161+
162+
prop.set(ctxt, bean, null);
163+
assertSame(orig, bean.values);
164+
assertSame(bean, prop.setAndReturn(ctxt, bean, null));
165+
assertSame(orig, bean.values);
166+
167+
// but non-null values still set
168+
List<String> other = Arrays.asList("b");
169+
prop.set(ctxt, bean, other);
170+
assertSame(other, bean.values);
171+
}
172+
}
173+
174+
// Skipped null needs no fallback setter, so must not fail even if there is none
175+
@Test
176+
public void testSkipNullsForCreatorWithoutSetter() throws Exception
177+
{
178+
CreatorOnlyBean bean = new CreatorOnlyBean("a");
179+
MAPPER.readerForUpdating(bean).readValue(a2q("{'value':null,'name':'x'}"));
180+
assertEquals("x", bean.name);
181+
assertEquals("a", bean.getValue());
182+
}
183+
}

0 commit comments

Comments
 (0)