Skip to content

Commit 3594e63

Browse files
Merge branch '3.x' into feature/512-json-wrapped-annotation
2 parents 93391ac + 8c54c01 commit 3594e63

7 files changed

Lines changed: 84 additions & 84 deletions

File tree

release-notes/CREDITS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,8 @@ Viktor Szathmáry (@phraktle)
229229
* Reported #5115: `@JsonUnwrapped` Record deserialization can't handle name collision
230230
(reported by Viktor S)
231231
[3.1.0]
232+
* Reported #5716: `@JsonUnwrapped` properties moved to end in 3.1.0
233+
[3.1.1]
232234

233235
Lee Chan Won (@chanwonlee)
234236
* Implemented #5223: Java Records missing type information with `DefaultTyping.NON_FINAL`

release-notes/VERSION

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ Versions: 3.x (for earlier see VERSION-2.x)
1515
#5710: `ObjectWriter` transforms polymorphic types into a tree incompletely
1616
(reported by Brandon S)
1717
(fix by @cowtowncoder, w/ Claude code)
18+
#5716: `@JsonUnwrapped` properties moved to end in 3.1.0
19+
(reported by Viktor S)
20+
(fix by @cowtowncoder, w/ Claude code)
1821

1922
3.1.0 (23-Feb-2026)
2023

src/main/java/tools/jackson/databind/introspect/POJOPropertiesCollector.java

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import tools.jackson.databind.cfg.MapperConfig;
1414
import tools.jackson.databind.deser.impl.UnwrappedPropertyHandler;
1515
import tools.jackson.databind.util.ClassUtil;
16+
import tools.jackson.databind.util.NameTransformer;
1617
import tools.jackson.databind.util.RecordUtil;
1718

1819
/**
@@ -1058,13 +1059,20 @@ private void _addCreatorParams(Map<String, POJOPropertyBuilder> props,
10581059

10591060
// First: check "Unwrapped" unless explicit name
10601061
if (!hasExplicit) {
1061-
var unwrapper = _annotationIntrospector.findUnwrappingNameTransformer(_config, param);
1062+
NameTransformer unwrapper = _annotationIntrospector.findUnwrappingNameTransformer(_config, param);
10621063
if (unwrapper != null) {
1063-
// If unwrapping, can use regardless of name; we will use a placeholder name
1064-
// anyway to try to avoid name conflicts.
1065-
PropertyName name = UnwrappedPropertyHandler.creatorParamName(param.getIndex());
1066-
final POJOPropertyBuilder prop = _property(props, name);
1067-
prop.addCtor(param, name, false, true, false);
1064+
// If unwrapping, use a placeholder name to avoid name conflicts during
1065+
// deserialization. Store the (possibly field-renamed) implicit name as
1066+
// the internal name so _sortProperties() can place this property at its
1067+
// declaration position without re-invoking the annotation introspector.
1068+
// [databind#5716]
1069+
final PropertyName placeholder = UnwrappedPropertyHandler.creatorParamName(param.getIndex());
1070+
final PropertyName internalName = hasImplicit ? implName : placeholder;
1071+
final POJOPropertyBuilder prop = new POJOPropertyBuilder(_config,
1072+
_annotationIntrospector, _forSerialization, internalName, placeholder);
1073+
prop._unwrapped = true;
1074+
props.put(placeholder.getSimpleName(), prop);
1075+
prop.addCtor(param, placeholder, false, true, false);
10681076
creatorProps.add(prop);
10691077
continue;
10701078
}
@@ -1821,6 +1829,17 @@ protected void _sortProperties(Map<String, POJOPropertyBuilder> props)
18211829
// 16-Jan-2016, tatu: Related to [databind#1317], make sure not to accidentally
18221830
// add back pruned creator properties!
18231831

1832+
// [databind#5716]: @JsonUnwrapped creator params use a placeholder name to avoid
1833+
// name conflicts during deserialization. The actual getter property name is stored
1834+
// as internalName in _addCreatorParams(), so use it here for correct ordering.
1835+
if (prop.isUnwrapped()) {
1836+
String internalName = prop.getInternalName();
1837+
POJOPropertyBuilder pb = all.get(internalName);
1838+
if (pb != null) {
1839+
ordered.put(internalName, pb);
1840+
continue;
1841+
}
1842+
}
18241843
String name = prop.getName();
18251844
// 27-Nov-2019, tatu: Not sure why, but we should NOT remove it from `all` tho:
18261845
// if (all.remove(name) != null) {

src/main/java/tools/jackson/databind/introspect/POJOPropertyBuilder.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,17 @@ public class POJOPropertyBuilder
7171
*/
7272
protected transient AnnotationIntrospector.ReferenceProperty _referenceInfo;
7373

74+
/**
75+
* Flag that indicates this property is marked with {@code @JsonUnwrapped}.
76+
* When set on a creator (constructor) parameter, the property uses a
77+
* placeholder as its external name to avoid conflicts during deserialization,
78+
* while {@link #_internalName} holds the actual implicit parameter name used
79+
* for declaration-order sorting.
80+
*
81+
* @since 3.1.1
82+
*/
83+
protected boolean _unwrapped;
84+
7485
public POJOPropertyBuilder(MapperConfig<?> config, AnnotationIntrospector ai,
7586
boolean forSerialization, PropertyName internalName) {
7687
this(config, ai, forSerialization, internalName, internalName);
@@ -97,6 +108,7 @@ protected POJOPropertyBuilder(POJOPropertyBuilder src, PropertyName newName)
97108
_getters = src._getters;
98109
_setters = src._setters;
99110
_forSerialization = src._forSerialization;
111+
_unwrapped = src._unwrapped;
100112
}
101113

102114
/*
@@ -400,6 +412,13 @@ public boolean hasFieldAndNothingElse() {
400412
@Override
401413
public boolean hasConstructorParameter() { return _ctorParameters != null; }
402414

415+
/**
416+
* Returns {@code true} if this property is marked with {@code @JsonUnwrapped}.
417+
*
418+
* @since 3.1
419+
*/
420+
public boolean isUnwrapped() { return _unwrapped; }
421+
403422
@Override
404423
public boolean couldDeserialize() {
405424
return (_ctorParameters != null)
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package tools.jackson.databind.records;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import com.fasterxml.jackson.annotation.JsonUnwrapped;
6+
7+
import tools.jackson.databind.MapperFeature;
8+
import tools.jackson.databind.ObjectMapper;
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+
14+
// [databind#5716] @JsonUnwrapped record properties should stay at declaration position
15+
public class RecordUnwrappedOrdering5716Test extends DatabindTestUtil
16+
{
17+
record Name(String first, String last) { }
18+
record Row(long time, @JsonUnwrapped Name name, double score) { }
19+
20+
private final ObjectMapper MAPPER = JsonMapper.builder()
21+
.disable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY)
22+
.enable(MapperFeature.SORT_CREATOR_PROPERTIES_FIRST)
23+
.build();
24+
25+
@Test
26+
void unwrappedRecordShouldKeepDeclarationOrder() throws Exception
27+
{
28+
Row input = new Row(1L, new Name("a", "b"), 2.5d);
29+
30+
String json = MAPPER.writeValueAsString(input);
31+
32+
assertEquals(a2q("{'time':1,'first':'a','last':'b','score':2.5}"), json);
33+
}
34+
}

src/test/java/tools/jackson/databind/records/RecordWithJsonUnwrappedTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ void unwrappedPojoShouldRoundTrip() throws Exception
7676
input.a.a = 1;
7777
input.a.b = 2;
7878

79-
String json = MAPPER.writeValueAsString(input);
79+
String json = MAPPER.writeValueAsString(input);
8080
BarPojo5115 output = MAPPER.readValue(json, BarPojo5115.class);
8181

8282
assertEquals(4, output.c);

src/test/java/tools/jackson/databind/tofix/SetterlessProperties501Test.java

Lines changed: 0 additions & 77 deletions
This file was deleted.

0 commit comments

Comments
 (0)