Skip to content

Commit 74e6b7d

Browse files
authored
Fix #171: allow keeping Record field declaration order for serialization (#174)
1 parent 0ccaac1 commit 74e6b7d

File tree

4 files changed

+61
-9
lines changed

4 files changed

+61
-9
lines changed

jr-objects/src/main/java/com/fasterxml/jackson/jr/ob/JSON.java

+11
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,17 @@ public enum Feature
242242
*/
243243
WRITE_READONLY_BEAN_PROPERTIES(true, true),
244244

245+
/**
246+
* Feature that determines whether record fields are serialized in declaration
247+
* order (enabled) or not (disabled). If disabled, record fields are serialized
248+
* same way as POJO properties, that is, alphabetically sorted.
249+
*<p>
250+
* Feature is disabled by default for backwards compatibility reasons.
251+
*
252+
* @since 2.19
253+
*/
254+
WRITE_RECORD_FIELDS_IN_DECLARATION_ORDER(false, true),
255+
245256
/**
246257
* Feature that determines whether access to {@link java.lang.reflect.Method}s and
247258
* {@link java.lang.reflect.Constructor}s that are used with dynamically

jr-objects/src/main/java/com/fasterxml/jackson/jr/ob/impl/BeanPropertyIntrospector.java

+29-8
Original file line numberDiff line numberDiff line change
@@ -51,20 +51,32 @@ private POJODefinition _introspectDefinition(Class<?> beanType,
5151
// For Serialization OTOH we need sorting (although would probably
5252
// be better to sort after the fact, maybe in future)
5353

54-
Map<String,PropBuilder> propsByName = forSerialization ?
55-
new TreeMap<>() : new LinkedHashMap<>();
56-
54+
Map<String,PropBuilder> propsByName;
55+
// 04-Nov-2024, tatu [jackson-jr#171] May need to retain order
56+
// for Record serialization too
57+
final boolean recordSerInDeclOrder = isRecord && forSerialization
58+
&& JSON.Feature.WRITE_RECORD_FIELDS_IN_DECLARATION_ORDER.isEnabled(features);
59+
60+
// Alphabetic ordering unnecessary for Deserialization (and some serialization too)
61+
if (forSerialization && !recordSerInDeclOrder) {
62+
propsByName = new TreeMap<>();
63+
} else {
64+
propsByName = new LinkedHashMap<>();
65+
}
66+
5767
final BeanConstructors constructors;
5868
if (forSerialization) {
69+
if (recordSerInDeclOrder) {
70+
Constructor<?> canonical = _getCanonicalRecordConstructor(beanType);
71+
for (Parameter ctorParam : canonical.getParameters()) {
72+
_propFrom(propsByName, ctorParam.getName());
73+
}
74+
}
5975
constructors = null;
6076
} else {
6177
constructors = new BeanConstructors(beanType);
6278
if (isRecord) {
63-
Constructor<?> canonical = RecordsHelpers.findCanonicalConstructor(beanType);
64-
if (canonical == null) { // should never happen
65-
throw new IllegalArgumentException(
66-
"Unable to find canonical constructor of Record type `"+beanType.getClass().getName()+"`");
67-
}
79+
Constructor<?> canonical = _getCanonicalRecordConstructor(beanType);
6880
constructors.addRecordConstructor(canonical);
6981
// And then let's "seed" properties to ensure correct ordering
7082
// of Properties wrt Canonical constructor parameters:
@@ -105,6 +117,15 @@ private POJODefinition _introspectDefinition(Class<?> beanType,
105117
return new POJODefinition(beanType, props, constructors);
106118
}
107119

120+
private Constructor<?> _getCanonicalRecordConstructor(Class<?> beanType) {
121+
Constructor<?> canonical = RecordsHelpers.findCanonicalConstructor(beanType);
122+
if (canonical == null) { // should never happen
123+
throw new IllegalArgumentException(
124+
"Unable to find canonical constructor of Record type `"+beanType.getClass().getName()+"`");
125+
}
126+
return canonical;
127+
}
128+
108129
private static void _introspect(Class<?> currType, Map<String, PropBuilder> props,
109130
int features, boolean isRecord)
110131
{

jr-record-test/src/test-jdk17/java/jr/Java17RecordTest.java

+19
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ public record WrapperRecord(Cow cow, String hello) {
2222
public record RecordWithWrapper(Cow cow, Wrapper nested, int someInt) {
2323
}
2424

25+
// [jackson-jr#171]: Whether to serialize Records in declaration or alphabetical order
26+
public record RecordNonAlphabetic171(int c, int b, int a) {
27+
}
28+
2529
record SingleIntRecord(int value) { }
2630
record SingleLongRecord(long value) { }
2731
record SingleStringRecord(String value) { }
@@ -162,4 +166,19 @@ public void testSingleFieldRecords() throws Exception {
162166
assertEquals("{\"value\":\"abc\"}", json);
163167
assertEquals(inputStr, jsonHandler.beanFrom(SingleStringRecord.class, json));
164168
}
169+
170+
// [jackson-jr#171]: Whether to serialize Records in declaration or alphabetical order
171+
public void testRecordFieldWriteOrder() throws Exception
172+
{
173+
RecordNonAlphabetic171 input = new RecordNonAlphabetic171(1, 2, 3);
174+
175+
// Alphabetical order:
176+
assertEquals("{\"a\":3,\"b\":2,\"c\":1}",
177+
jsonHandler.without(JSON.Feature.WRITE_RECORD_FIELDS_IN_DECLARATION_ORDER).asString(input));
178+
179+
// Declaration order:
180+
assertEquals("{\"c\":1,\"b\":2,\"a\":3}",
181+
jsonHandler.with(JSON.Feature.WRITE_RECORD_FIELDS_IN_DECLARATION_ORDER).asString(input));
182+
}
165183
}
184+

release-notes/VERSION-2.x

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ Modules:
1313

1414
2.19.0 (not yet released)
1515

16-
-
16+
#171: Add a `JSON.Feature.WRITE_RECORD_FIELDS_IN_DECLARATION_ORDER` for
17+
retaining Serialization order of Java Records (instead of alphabetic)
1718

1819
2.18.1 (28-Oct-2024)
1920

0 commit comments

Comments
 (0)