Skip to content

Commit c3d56dd

Browse files
authored
Backport #5967: "Renamed @JsonIgnored setters can deserialize via private fields" (#5968)
1 parent 635adf6 commit c3d56dd

6 files changed

Lines changed: 211 additions & 9 deletions

File tree

‎release-notes/CREDITS-2.x‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1922,6 +1922,8 @@ Omkhar Arasaratnam (@omkhar)
19221922
(2.18.8)
19231923
* Reported #5951: Improve `InetSocketAddress` deserialization
19241924
(2.18.8)
1925+
* Contributed fix for #5967: Renamed `@JsonIgnore`d setters can deserialize via private fields
1926+
(2.21.4)
19251927
19261928
Liam Feid (@fxshlein)
19271929
* Contributed #1467: Support `@JsonUnwrapped` with `@JsonCreator`

‎release-notes/VERSION-2.x‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ Project: jackson-databind
1212
(reported by Omkhar A)
1313
#5951: Improve `InetSocketAddress` deserialization
1414
(reported by Omkhar A)
15+
#5967: Renamed `@JsonIgnore`d setters can deserialize via private fields
16+
(fixed by Omkhar A)
1517

1618
2.21.3 (28-Apr-2026)
1719

‎src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertiesCollector.java‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1586,6 +1586,11 @@ protected void _renameProperties(Map<String, POJOPropertyBuilder> props)
15861586
if (isRecordType() || !prop.anyExplicitsWithoutIgnoral()) {
15871587
continue;
15881588
}
1589+
// [databind#5967]: Strip inferred non-visible field mutators to preserve @JsonIgnore
1590+
// semantics. The ignored name was collected because couldDeserialize()==false,
1591+
// meaning any retained fields are non-visible (kept only by INFER_PROPERTY_MUTATORS).
1592+
// Removing them ensures the renamed property remains read-only (serialization only).
1593+
prop.removeFields();
15891594
}
15901595

15911596
Collection<PropertyName> l = prop.findExplicitNames();
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.fasterxml.jackson.databind.records;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import com.fasterxml.jackson.annotation.JsonProperty;
6+
7+
import com.fasterxml.jackson.databind.ObjectMapper;
8+
import com.fasterxml.jackson.databind.testutil.DatabindTestUtil;
9+
10+
import static org.junit.jupiter.api.Assertions.*;
11+
12+
// [databind#5967] Records take a different path in
13+
// POJOPropertiesCollector#_renameProperties (always skipped by isRecordType()
14+
// when the property name is in _ignoredPropertyNames), so the field-stripping
15+
// fix added for non-records must not regress record renaming.
16+
public class RecordRenamedPropertyIgnoreFieldBypass5967Test extends DatabindTestUtil
17+
{
18+
public record RenamedRecord(@JsonProperty("renamedProp") String prop) {}
19+
20+
private final ObjectMapper MAPPER = newJsonMapper();
21+
22+
@Test
23+
public void recordWithRenamedPropertyRoundTrips() throws Exception
24+
{
25+
RenamedRecord original = new RenamedRecord("someValue");
26+
String json = MAPPER.writeValueAsString(original);
27+
assertEquals("{\"renamedProp\":\"someValue\"}", json);
28+
29+
RenamedRecord result = MAPPER.readValue(json, RenamedRecord.class);
30+
assertEquals("someValue", result.prop());
31+
}
32+
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
package com.fasterxml.jackson.databind.introspect;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import com.fasterxml.jackson.annotation.*;
6+
7+
import com.fasterxml.jackson.databind.*;
8+
import com.fasterxml.jackson.databind.testutil.DatabindTestUtil;
9+
10+
import static org.junit.jupiter.api.Assertions.*;
11+
12+
/**
13+
* Tests covering the [databind#5398] follow-up: when a property's setter is
14+
* {@code @JsonIgnore}d (and the getter carries {@code @JsonProperty} that
15+
* keeps the property non-ignored as a whole), the inferred field mutator
16+
* retained via {@code MapperFeature.INFER_PROPERTY_MUTATORS} must NOT be
17+
* used to bypass the ignored setter. The property should remain
18+
* serialization-only (read-only).
19+
*/
20+
public class JsonIgnoreSetterFieldBypass5398Test extends DatabindTestUtil
21+
{
22+
static class RenamedGetterIgnoredSetter {
23+
private String prop;
24+
25+
@JsonProperty("renamedProp")
26+
public String getProp() {
27+
return prop;
28+
}
29+
30+
@JsonIgnore
31+
public void setProp(String prop) {
32+
this.prop = prop;
33+
}
34+
}
35+
36+
static class StandardGetterIgnoredSetter {
37+
private String prop;
38+
39+
@JsonProperty
40+
public String getProp() {
41+
return prop;
42+
}
43+
44+
@JsonIgnore
45+
public void setProp(String prop) {
46+
this.prop = prop;
47+
}
48+
}
49+
50+
// Sibling shape: READ_ONLY access on getter rather than @JsonIgnore on setter.
51+
static class ReadOnlyRenamedGetter {
52+
private String prop;
53+
54+
@JsonProperty(value = "renamedProp", access = JsonProperty.Access.READ_ONLY)
55+
public String getProp() {
56+
return prop;
57+
}
58+
59+
public void setProp(String prop) {
60+
this.prop = prop;
61+
}
62+
}
63+
64+
// 2.x defaults FAIL_ON_UNKNOWN_PROPERTIES to true; disable so the "not written"
65+
// outcome (rather than an exception) is what we assert on read paths.
66+
private final ObjectMapper MAPPER = jsonMapperBuilder()
67+
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
68+
.build();
69+
70+
@Test
71+
public void renamedGetterStillSerializes() throws Exception
72+
{
73+
RenamedGetterIgnoredSetter bean = new RenamedGetterIgnoredSetter();
74+
bean.setProp("someValue");
75+
assertEquals("{\"renamedProp\":\"someValue\"}",
76+
MAPPER.writeValueAsString(bean));
77+
}
78+
79+
@Test
80+
public void standardGetterStillSerializes() throws Exception
81+
{
82+
StandardGetterIgnoredSetter bean = new StandardGetterIgnoredSetter();
83+
bean.setProp("someValue");
84+
assertEquals("{\"prop\":\"someValue\"}",
85+
MAPPER.writeValueAsString(bean));
86+
}
87+
88+
// The renamed property must be read-only: @JsonIgnore on the setter
89+
// blocks the write, and the inferred field mutator must not bypass it.
90+
@Test
91+
public void renamedIgnoredSetterDoesNotBypassViaField() throws Exception
92+
{
93+
RenamedGetterIgnoredSetter result = MAPPER.readValue(
94+
"{\"renamedProp\":\"someValue\"}",
95+
RenamedGetterIgnoredSetter.class);
96+
assertNotNull(result);
97+
assertNull(result.getProp());
98+
}
99+
100+
@Test
101+
public void standardIgnoredSetterDoesNotBypassViaField() throws Exception
102+
{
103+
StandardGetterIgnoredSetter result = MAPPER.readValue(
104+
"{\"prop\":\"someValue\"}",
105+
StandardGetterIgnoredSetter.class);
106+
assertNotNull(result);
107+
assertNull(result.getProp());
108+
}
109+
110+
// Control: feeding the implicit field name (rather than the renamed
111+
// public name) also does not write the field.
112+
@Test
113+
public void implicitFieldNameDoesNotWriteWhenSetterIgnored() throws Exception
114+
{
115+
RenamedGetterIgnoredSetter result = MAPPER.readerFor(RenamedGetterIgnoredSetter.class)
116+
.without(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
117+
.readValue("{\"prop\":\"someValue\"}");
118+
assertNotNull(result);
119+
assertNull(result.getProp());
120+
}
121+
122+
// Bypass must also stay closed when INFER_PROPERTY_MUTATORS is disabled,
123+
// i.e. the fix is not just an artifact of inference being on.
124+
@Test
125+
public void inferMutatorsDisabledStillBlocksWrite() throws Exception
126+
{
127+
ObjectMapper mapper = jsonMapperBuilder()
128+
.disable(MapperFeature.INFER_PROPERTY_MUTATORS)
129+
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
130+
.build();
131+
RenamedGetterIgnoredSetter result = mapper.readValue(
132+
"{\"renamedProp\":\"someValue\"}",
133+
RenamedGetterIgnoredSetter.class);
134+
assertNotNull(result);
135+
assertNull(result.getProp());
136+
}
137+
138+
// Sibling: @JsonProperty(access = READ_ONLY) on the renamed getter must
139+
// produce the same outcome — serialize under the new name, not write
140+
// back via the inferred field mutator.
141+
@Test
142+
public void readOnlyRenamedGetterIsSerializeOnly() throws Exception
143+
{
144+
ReadOnlyRenamedGetter bean = new ReadOnlyRenamedGetter();
145+
bean.setProp("someValue");
146+
assertEquals("{\"renamedProp\":\"someValue\"}",
147+
MAPPER.writeValueAsString(bean));
148+
149+
ReadOnlyRenamedGetter result = MAPPER.readValue(
150+
"{\"renamedProp\":\"someValue\"}",
151+
ReadOnlyRenamedGetter.class);
152+
assertNotNull(result);
153+
assertNull(result.getProp());
154+
}
155+
156+
}

‎src/test/java/com/fasterxml/jackson/databind/introspect/JsonPropertyRename5398Test.java‎

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,14 @@ public void testRenamedPropertyWithIgnoredSetter5398() throws Exception
5353
// Should serialize with renamed property
5454
assertEquals("{\"renamedProp\":\"someValue\"}", json);
5555

56-
// Should be able to deserialize back (setter is ignored, so field remains default)
57-
TestRename5398 result = MAPPER.readValue(json, TestRename5398.class);
56+
// @JsonIgnore on the setter prevents write access to the backing field
57+
// ([databind#5967] fix: inferred non-visible field mutator stripped when setter is @JsonIgnore).
58+
// Deserialization of "renamedProp" is blocked; field stays at its default (null).
59+
TestRename5398 result = MAPPER.readerFor(TestRename5398.class)
60+
.without(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
61+
.readValue(json);
5862
assertNotNull(result);
59-
// The setter is ignored but the property is still considered read-write
60-
assertEquals("someValue", result.getProp());
63+
assertNull(result.getProp());
6164
}
6265

6366
@Test
@@ -68,13 +71,15 @@ public void testStandardPropertyWithIgnoredSetter5398() throws Exception
6871

6972
String json = MAPPER.writeValueAsString(original);
7073

71-
// Should serialize with renamed property
74+
// Should serialize under the implicit property name
7275
assertEquals("{\"prop\":\"someValue\"}", json);
7376

74-
// Should be able to deserialize back (setter is ignored, so field remains default)
75-
TestStd5398 result = MAPPER.readValue(json, TestStd5398.class);
77+
// @JsonIgnore on the setter prevents write access to the backing field.
78+
// Field stays at its default (null) after deserialization.
79+
TestStd5398 result = MAPPER.readerFor(TestStd5398.class)
80+
.without(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
81+
.readValue(json);
7682
assertNotNull(result);
77-
// The setter is ignored but the property is still considered read-write
78-
assertEquals("someValue", result.getProp());
83+
assertNull(result.getProp());
7984
}
8085
}

0 commit comments

Comments
 (0)