Skip to content

Commit 118fba4

Browse files
authored
Fix #650: @JsonUnwrapped prevents checks for DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES (#5504)
1 parent e74aa50 commit 118fba4

File tree

8 files changed

+418
-78
lines changed

8 files changed

+418
-78
lines changed

.github/workflows/main.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ jobs:
117117
}
118118
END {
119119
if (total_missed + total_covered > 0)
120-
printf "%.1f", (total_covered * 100.0) / (total_missed + total_covered)
120+
printf "%.2f", (total_covered * 100.0) / (total_missed + total_covered)
121121
}' "$csv_file"
122122
}
123123
@@ -140,8 +140,8 @@ jobs:
140140
}
141141
142142
# Convert decimal to percentage and round to 1 decimal place
143-
COVERAGE=$(awk -v cov="${{ steps.jacoco.outputs.coverage }}" 'BEGIN { printf "%.1f", cov * 100 }')
144-
BRANCHES=$(awk -v br="${{ steps.jacoco.outputs.branches }}" 'BEGIN { printf "%.1f", br * 100 }')
143+
COVERAGE=$(awk -v cov="${{ steps.jacoco.outputs.coverage }}" 'BEGIN { printf "%.2f", cov * 100 }')
144+
BRANCHES=$(awk -v br="${{ steps.jacoco.outputs.branches }}" 'BEGIN { printf "%.2f", br * 100 }')
145145
146146
# Check if base coverage artifact was downloaded and calculate deltas
147147
HAS_DELTA=false

release-notes/CREDITS

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -115,16 +115,16 @@ Fouad Almalki (@Eng-Fouad)
115115
* Contributed fix for 5442: Make `JsonMapper/ObjectMapper` fully proxyable by CGLIB
116116
[3.1.0]
117117

118-
Oliver Drotbohm (@odrotbohm)
119-
* Requested #1196: Add opt-in error collection for deserialization
120-
[3.1.0]
121-
122-
@sri-adarsh-kumar
123-
* Contributed #1196: Add opt-in error collection for deserialization
118+
Konstantin Labun (@kulabun)
119+
* Reported #650: `@JsonUnwrapped` prevents checks for
120+
`DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES`
124121
[3.1.0]
125122

126123
@JacksonJang
127-
* Contributed fix for #1516: Problem with multi-argument Creator with
124+
* Contributed fix for #650: `@JsonUnwrapped` prevents checks for
125+
`DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES`
126+
[3.1.0]
127+
* Contributed fix for #1516: Problem with multi-argument Creator with
128128
`@JsonBackReference` property
129129
[3.1.0]
130130
* Contributed fix for #2686: `@JsonBackReference` does not work with a builder
@@ -139,6 +139,14 @@ Oliver Drotbohm (@odrotbohm)
139139
`java.util.Map` serialization via property annotation
140140
[3.1.0]
141141

142+
Oliver Drotbohm (@odrotbohm)
143+
* Requested #1196: Add opt-in error collection for deserialization
144+
[3.1.0]
145+
146+
@sri-adarsh-kumar
147+
* Contributed #1196: Add opt-in error collection for deserialization
148+
[3.1.0]
149+
142150
Viktor Szathmáry (@phraktle)
143151
* Reported #5115: `@JsonUnwrapped` Record deserialization can't handle name collision
144152
(reported by Viktor S)

release-notes/VERSION

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ Versions: 3.x (for earlier see VERSION-2.x)
88

99
3.1.0 (not yet released)
1010

11+
#650: `@JsonUnwrapped` prevents checks for
12+
`DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES`
13+
(reported by Konstantin L)
14+
(fix by @JacksonJang)
1115
#1196: Add opt-in error collection for deserialization
1216
(requested by @odrotbohm)
1317
(contributed by @sri-adarsh-kumar)

src/main/java/tools/jackson/databind/deser/bean/BeanDeserializer.java

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import tools.jackson.databind.deser.SettableBeanProperty;
1212
import tools.jackson.databind.deser.UnresolvedForwardReference;
1313
import tools.jackson.databind.deser.impl.*;
14+
import tools.jackson.databind.exc.UnrecognizedPropertyException;
1415
import tools.jackson.databind.util.ClassUtil;
1516
import tools.jackson.databind.util.IgnorePropertiesUtil;
1617
import tools.jackson.databind.util.NameTransformer;
@@ -928,13 +929,17 @@ protected Object deserializeWithUnwrapped(JsonParser p, DeserializationContext c
928929
// 29-Nov-2016, tatu: probably should try to avoid sending content
929930
// both to any setter AND buffer... but, for now, the only thing
930931
// we can do.
931-
// how about any setter? We'll get copies but...
932-
if (_anySetter == null) {
933-
// but... others should be passed to unwrapped property deserializers
932+
// 19-Dec-2025: [databind#650] We can now distinguish the cases
933+
if (_unwrappedPropertyHandler.hasUnwrappedProperty(propName)) {
934934
tokens.writeName(propName);
935935
tokens.copyCurrentStructure(p);
936936
continue;
937937
}
938+
// how about any setter? We'll get copies but...
939+
if (_anySetter == null) {
940+
handleUnknownVanilla(p, ctxt, bean, propName);
941+
continue;
942+
}
938943
// Need to copy to a separate buffer first
939944
TokenBuffer b2 = ctxt.bufferAsCopyOfValue(p);
940945
tokens.writeName(propName);
@@ -992,11 +997,12 @@ protected Object deserializeWithUnwrapped(JsonParser p, DeserializationContext c
992997
// 29-Nov-2016, tatu: probably should try to avoid sending content
993998
// both to any setter AND buffer... but, for now, the only thing
994999
// we can do.
995-
// how about any setter? We'll get copies but...
996-
if (_anySetter == null) {
997-
// but... others should be passed to unwrapped property deserializers
1000+
// 19-Dec-2025: [databind#650] We can now distinguish the cases
1001+
if (_unwrappedPropertyHandler.hasUnwrappedProperty(propName)) {
9981002
tokens.writeName(propName);
9991003
tokens.copyCurrentStructure(p);
1004+
} else if (_anySetter == null) {
1005+
handleUnknownVanilla(p, ctxt, bean, propName);
10001006
} else {
10011007
// Need to copy to a separate buffer first
10021008
TokenBuffer b2 = ctxt.bufferAsCopyOfValue(p);
@@ -1072,11 +1078,22 @@ protected Object deserializeUsingPropertyBasedWithUnwrapped(JsonParser p, Deseri
10721078
// 29-Nov-2016, tatu: probably should try to avoid sending content
10731079
// both to any setter AND buffer... but, for now, the only thing
10741080
// we can do.
1075-
// how about any setter? We'll get copies but...
1076-
if (_anySetter == null) {
1077-
// but... others should be passed to unwrapped property deserializers
1081+
// 19-Dec-2025: [databind#650] We can now distinguish the cases
1082+
// but... others should be passed to unwrapped property deserializers
1083+
if (_unwrappedPropertyHandler.hasUnwrappedProperty(propName)) {
10781084
tokens.writeName(propName);
10791085
tokens.copyCurrentStructure(p);
1086+
} else if (_anySetter == null) {
1087+
// [databind#650]: priority: @JsonIgnoreProperties > FAIL_ON_UNKNOWN_PROPERTIES
1088+
if (_ignoreAllUnknown) {
1089+
p.skipChildren();
1090+
} else if (IgnorePropertiesUtil.shouldIgnore(propName, _ignorableProps, _includableProps)) {
1091+
handleIgnoredProperty(p, ctxt, handledType(), propName);
1092+
} else if (ctxt.isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)) {
1093+
throw UnrecognizedPropertyException.from(p, handledType(), propName, getKnownPropertyNames());
1094+
} else {
1095+
p.skipChildren();
1096+
}
10801097
} else {
10811098
// Need to copy to a separate buffer first
10821099
TokenBuffer b2 = ctxt.bufferAsCopyOfValue(p);

src/main/java/tools/jackson/databind/deser/bean/BeanDeserializerBase.java

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -635,9 +635,12 @@ public void resolve(DeserializationContext ctxt)
635635
_nonStandardCreation = true;
636636
}
637637

638-
_unwrappedPropertyHandler = unwrapped;
639638
if (unwrapped != null) { // we consider this non-standard, to offline handling
640639
_nonStandardCreation = true;
640+
// [databind#650]: Initialize nested property names cache for hasUnwrappedProperty()
641+
_unwrappedPropertyHandler = unwrapped.initializedNestedPropertyNames();
642+
} else {
643+
_unwrappedPropertyHandler = null;
641644
}
642645
// may need to disable vanilla processing, if unwrapped handling was enabled...
643646
_vanillaProcessing = _vanillaProcessing && !_nonStandardCreation;
@@ -1132,14 +1135,46 @@ public ObjectIdReader getObjectIdReader(DeserializationContext ctxt) {
11321135
return _objectIdReader;
11331136
}
11341137

1138+
/**
1139+
* Accessor for checking if the POJO handled by this deserializer has given
1140+
* physical property (regular or unwrapped, not including
1141+
* "any properties".
1142+
*
1143+
* @param propertyName Property to check
1144+
*
1145+
* @return True if the POJO handled by this deserializer has given
1146+
* physical property (regular or unwrapped); not including
1147+
* "any properties"
1148+
*/
11351149
public boolean hasProperty(String propertyName) {
1136-
return _beanProperties.findDefinition(propertyName) != null;
1150+
// normal properties
1151+
if (_beanProperties.findDefinition(propertyName) != null) {
1152+
return true;
1153+
}
1154+
// 19-Dec-2025: [databind#650] Check unwrapped properties too.
1155+
if (_unwrappedPropertyHandler != null) {
1156+
if (_unwrappedPropertyHandler.hasUnwrappedProperty(propertyName)) {
1157+
return true;
1158+
}
1159+
}
1160+
// 19-Dec-2025: [databind#650] but should "any-setter" be considered too?
1161+
// if (_anySetter != null) {
1162+
// return true;
1163+
// }
1164+
return false;
11371165
}
11381166

11391167
public boolean hasViews() {
11401168
return _needViewProcesing;
11411169
}
11421170

1171+
/**
1172+
* @since 3.1
1173+
*/
1174+
public boolean hasAnySetter() {
1175+
return _anySetter != null;
1176+
}
1177+
11431178
/**
11441179
* Accessor for checking number of deserialized properties.
11451180
*/
@@ -1149,7 +1184,7 @@ public int getPropertyCount() {
11491184

11501185
@Override
11511186
public Collection<Object> getKnownPropertyNames() {
1152-
ArrayList<Object> names = new ArrayList<Object>();
1187+
ArrayList<Object> names = new ArrayList<>();
11531188
for (SettableBeanProperty prop : _beanProperties) {
11541189
names.add(prop.getName());
11551190
}
@@ -1164,6 +1199,23 @@ public Collection<Object> getKnownPropertyNames() {
11641199
return names;
11651200
}
11661201

1202+
/**
1203+
* Method to collect all property names including nested unwrapped properties
1204+
*
1205+
* @param names (not null) Set to add property names to; for both regular
1206+
* and "any" properties.
1207+
*
1208+
* @since 3.1
1209+
*/
1210+
public void collectAllPropertyNamesTo(Set<String> names) {
1211+
for (SettableBeanProperty prop : _beanProperties) {
1212+
names.add(prop.getName());
1213+
}
1214+
if (_unwrappedPropertyHandler != null) {
1215+
_unwrappedPropertyHandler.collectNestedPropertyNamesTo(names);
1216+
}
1217+
}
1218+
11671219
@Override
11681220
public JavaType getValueType() { return _beanType; }
11691221

@@ -1202,14 +1254,15 @@ public Iterator<SettableBeanProperty> creatorProperties()
12021254

12031255
public SettableBeanProperty findProperty(PropertyName propertyName)
12041256
{
1205-
// TODO: start matching full name?
12061257
return findProperty(propertyName.getSimpleName());
12071258
}
12081259

12091260
/**
12101261
* Accessor for finding the property with given name, if POJO
12111262
* has one. Name used is the external name, i.e. name used
12121263
* in external data representation (JSON).
1264+
*<p>
1265+
* NOTE: does NOT match "unwrapped" properties POJO contains (if any).
12131266
*/
12141267
protected SettableBeanProperty findProperty(String propertyName)
12151268
{

src/main/java/tools/jackson/databind/deser/impl/UnwrappedPropertyHandler.java

Lines changed: 106 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
import tools.jackson.core.*;
66
import tools.jackson.databind.DeserializationContext;
77
import tools.jackson.databind.PropertyName;
8+
import tools.jackson.databind.ValueDeserializer;
89
import tools.jackson.databind.deser.SettableBeanProperty;
10+
import tools.jackson.databind.deser.bean.BeanDeserializerBase;
911
import tools.jackson.databind.deser.bean.PropertyValueBuffer;
1012
import tools.jackson.databind.util.NameTransformer;
1113
import tools.jackson.databind.util.TokenBuffer;
@@ -28,15 +30,44 @@ public class UnwrappedPropertyHandler
2830
protected final List<SettableBeanProperty> _creatorProperties;
2931
protected final List<SettableBeanProperty> _properties;
3032

31-
public UnwrappedPropertyHandler() {
33+
/**
34+
* Set of all nested property names from unwrapped deserializers.
35+
*/
36+
protected final Set<String> _nestedPropertyNames;
37+
38+
/**
39+
* Flag indicating whether any unwrapped deserializer has an AnySetter,
40+
* which means it can handle any property name.
41+
*/
42+
protected final boolean _hasNestedAnySetter;
43+
44+
public UnwrappedPropertyHandler() {
3245
_creatorProperties = new ArrayList<>();
3346
_properties = new ArrayList<>();
47+
// placeholder: won't be modified in-place
48+
_nestedPropertyNames = Collections.emptySet();
49+
_hasNestedAnySetter = false;
3450
}
3551

3652
protected UnwrappedPropertyHandler(List<SettableBeanProperty> creatorProps,
37-
List<SettableBeanProperty> props) {
53+
List<SettableBeanProperty> props,
54+
Set<String> nestedPropertyNames,
55+
boolean hasNestedAnySetter) {
3856
_creatorProperties = creatorProps;
3957
_properties = props;
58+
_nestedPropertyNames = nestedPropertyNames;
59+
_hasNestedAnySetter = hasNestedAnySetter;
60+
}
61+
62+
/**
63+
* Creates a new UnwrappedPropertyHandler with initialized nested property names cache.
64+
*
65+
* @since 3.1
66+
*/
67+
public UnwrappedPropertyHandler initializedNestedPropertyNames() {
68+
Set<String> nestedNames = new HashSet<>();
69+
boolean hasAnySetter = _collectNestedPropertyNames(_properties, _creatorProperties, nestedNames);
70+
return new UnwrappedPropertyHandler(_creatorProperties, _properties, nestedNames, hasAnySetter);
4071
}
4172

4273
/**
@@ -53,10 +84,14 @@ public void addProperty(SettableBeanProperty property) {
5384
public UnwrappedPropertyHandler renameAll(DeserializationContext ctxt,
5485
NameTransformer transformer)
5586
{
56-
return new UnwrappedPropertyHandler(
57-
renameProperties(ctxt,_creatorProperties, transformer),
58-
renameProperties(ctxt, _properties, transformer)
59-
);
87+
List<SettableBeanProperty> renamedCreatorProps = renameProperties(ctxt, _creatorProperties, transformer);
88+
List<SettableBeanProperty> renamedProps = renameProperties(ctxt, _properties, transformer);
89+
90+
// Collect nested property names and check for AnySetter
91+
Set<String> nestedNames = new HashSet<>();
92+
boolean hasAnySetter = _collectNestedPropertyNames(renamedProps, renamedCreatorProps, nestedNames);
93+
94+
return new UnwrappedPropertyHandler(renamedCreatorProps, renamedProps, nestedNames, hasAnySetter);
6095
}
6196

6297
private List<SettableBeanProperty> renameProperties(DeserializationContext ctxt,
@@ -109,4 +144,69 @@ public Object processUnwrapped(JsonParser originalParser, DeserializationContext
109144
public static PropertyName creatorParamName(int index) {
110145
return new PropertyName(JSON_UNWRAPPED_NAME_PREFIX + index);
111146
}
147+
148+
/**
149+
* Method that checks if the given property name belongs to any unwrapped property.
150+
*
151+
* @return {@code true} if any nested deserializers has an "any-setter".
152+
*
153+
* @since 3.1
154+
*/
155+
public boolean hasUnwrappedProperty(String propName) {
156+
// If any nested deserializer has AnySetter, it can handle any property
157+
if (_hasNestedAnySetter) {
158+
return true;
159+
}
160+
return _nestedPropertyNames.contains(propName);
161+
}
162+
163+
/**
164+
* Collects all nested property names from unwrapped deserializers.
165+
*
166+
* @since 3.1
167+
*/
168+
public void collectNestedPropertyNamesTo(Set<String> names) {
169+
_collectNestedPropertyNames(_properties, _creatorProperties, names);
170+
}
171+
172+
/**
173+
* Helper method to collect nested property names.
174+
*
175+
* @return {@code true} if any property deserializer has AnySetter.
176+
*
177+
* @since 3.1
178+
*/
179+
private boolean _collectNestedPropertyNames(List<SettableBeanProperty> properties,
180+
List<SettableBeanProperty> creatorProperties,
181+
Set<String> names) {
182+
boolean hasAnySetter = false;
183+
for (SettableBeanProperty prop : properties) {
184+
if (_collectDeserializerPropertyNames(prop, names)) {
185+
hasAnySetter = true;
186+
}
187+
}
188+
for (SettableBeanProperty prop : creatorProperties) {
189+
if (_collectDeserializerPropertyNames(prop, names)) {
190+
hasAnySetter = true;
191+
}
192+
}
193+
return hasAnySetter;
194+
}
195+
196+
/**
197+
* Helper method to collect property names from a property's deserializer.
198+
*
199+
* @return {@code true} if the property deserializer has AnySetter.
200+
*/
201+
private boolean _collectDeserializerPropertyNames(SettableBeanProperty prop, Set<String> names) {
202+
if (prop != null) {
203+
ValueDeserializer<?> deser = prop.getValueDeserializer();
204+
if (deser instanceof BeanDeserializerBase bd) {
205+
// Recursively collect property names
206+
bd.collectAllPropertyNamesTo(names);
207+
return bd.hasAnySetter();
208+
}
209+
}
210+
return false;
211+
}
112212
}

0 commit comments

Comments
 (0)