From 74e6b7d8096af626cea15d0ee2bd48c6dfabada3 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Mon, 4 Nov 2024 19:24:07 -0800 Subject: [PATCH] Fix #171: allow keeping Record field declaration order for serialization (#174) --- .../com/fasterxml/jackson/jr/ob/JSON.java | 11 ++++++ .../jr/ob/impl/BeanPropertyIntrospector.java | 37 +++++++++++++++---- .../test-jdk17/java/jr/Java17RecordTest.java | 19 ++++++++++ release-notes/VERSION-2.x | 3 +- 4 files changed, 61 insertions(+), 9 deletions(-) diff --git a/jr-objects/src/main/java/com/fasterxml/jackson/jr/ob/JSON.java b/jr-objects/src/main/java/com/fasterxml/jackson/jr/ob/JSON.java index c02d2903..ed5666e1 100644 --- a/jr-objects/src/main/java/com/fasterxml/jackson/jr/ob/JSON.java +++ b/jr-objects/src/main/java/com/fasterxml/jackson/jr/ob/JSON.java @@ -242,6 +242,17 @@ public enum Feature */ WRITE_READONLY_BEAN_PROPERTIES(true, true), + /** + * Feature that determines whether record fields are serialized in declaration + * order (enabled) or not (disabled). If disabled, record fields are serialized + * same way as POJO properties, that is, alphabetically sorted. + *

+ * Feature is disabled by default for backwards compatibility reasons. + * + * @since 2.19 + */ + WRITE_RECORD_FIELDS_IN_DECLARATION_ORDER(false, true), + /** * Feature that determines whether access to {@link java.lang.reflect.Method}s and * {@link java.lang.reflect.Constructor}s that are used with dynamically diff --git a/jr-objects/src/main/java/com/fasterxml/jackson/jr/ob/impl/BeanPropertyIntrospector.java b/jr-objects/src/main/java/com/fasterxml/jackson/jr/ob/impl/BeanPropertyIntrospector.java index 80d3e7a6..a3770f80 100644 --- a/jr-objects/src/main/java/com/fasterxml/jackson/jr/ob/impl/BeanPropertyIntrospector.java +++ b/jr-objects/src/main/java/com/fasterxml/jackson/jr/ob/impl/BeanPropertyIntrospector.java @@ -51,20 +51,32 @@ private POJODefinition _introspectDefinition(Class beanType, // For Serialization OTOH we need sorting (although would probably // be better to sort after the fact, maybe in future) - Map propsByName = forSerialization ? - new TreeMap<>() : new LinkedHashMap<>(); - + Map propsByName; + // 04-Nov-2024, tatu [jackson-jr#171] May need to retain order + // for Record serialization too + final boolean recordSerInDeclOrder = isRecord && forSerialization + && JSON.Feature.WRITE_RECORD_FIELDS_IN_DECLARATION_ORDER.isEnabled(features); + + // Alphabetic ordering unnecessary for Deserialization (and some serialization too) + if (forSerialization && !recordSerInDeclOrder) { + propsByName = new TreeMap<>(); + } else { + propsByName = new LinkedHashMap<>(); + } + final BeanConstructors constructors; if (forSerialization) { + if (recordSerInDeclOrder) { + Constructor canonical = _getCanonicalRecordConstructor(beanType); + for (Parameter ctorParam : canonical.getParameters()) { + _propFrom(propsByName, ctorParam.getName()); + } + } constructors = null; } else { constructors = new BeanConstructors(beanType); if (isRecord) { - Constructor canonical = RecordsHelpers.findCanonicalConstructor(beanType); - if (canonical == null) { // should never happen - throw new IllegalArgumentException( -"Unable to find canonical constructor of Record type `"+beanType.getClass().getName()+"`"); - } + Constructor canonical = _getCanonicalRecordConstructor(beanType); constructors.addRecordConstructor(canonical); // And then let's "seed" properties to ensure correct ordering // of Properties wrt Canonical constructor parameters: @@ -105,6 +117,15 @@ private POJODefinition _introspectDefinition(Class beanType, return new POJODefinition(beanType, props, constructors); } + private Constructor _getCanonicalRecordConstructor(Class beanType) { + Constructor canonical = RecordsHelpers.findCanonicalConstructor(beanType); + if (canonical == null) { // should never happen + throw new IllegalArgumentException( +"Unable to find canonical constructor of Record type `"+beanType.getClass().getName()+"`"); + } + return canonical; + } + private static void _introspect(Class currType, Map props, int features, boolean isRecord) { diff --git a/jr-record-test/src/test-jdk17/java/jr/Java17RecordTest.java b/jr-record-test/src/test-jdk17/java/jr/Java17RecordTest.java index 4ec0c176..0b1d832f 100644 --- a/jr-record-test/src/test-jdk17/java/jr/Java17RecordTest.java +++ b/jr-record-test/src/test-jdk17/java/jr/Java17RecordTest.java @@ -22,6 +22,10 @@ public record WrapperRecord(Cow cow, String hello) { public record RecordWithWrapper(Cow cow, Wrapper nested, int someInt) { } + // [jackson-jr#171]: Whether to serialize Records in declaration or alphabetical order + public record RecordNonAlphabetic171(int c, int b, int a) { + } + record SingleIntRecord(int value) { } record SingleLongRecord(long value) { } record SingleStringRecord(String value) { } @@ -162,4 +166,19 @@ public void testSingleFieldRecords() throws Exception { assertEquals("{\"value\":\"abc\"}", json); assertEquals(inputStr, jsonHandler.beanFrom(SingleStringRecord.class, json)); } + + // [jackson-jr#171]: Whether to serialize Records in declaration or alphabetical order + public void testRecordFieldWriteOrder() throws Exception + { + RecordNonAlphabetic171 input = new RecordNonAlphabetic171(1, 2, 3); + + // Alphabetical order: + assertEquals("{\"a\":3,\"b\":2,\"c\":1}", + jsonHandler.without(JSON.Feature.WRITE_RECORD_FIELDS_IN_DECLARATION_ORDER).asString(input)); + + // Declaration order: + assertEquals("{\"c\":1,\"b\":2,\"a\":3}", + jsonHandler.with(JSON.Feature.WRITE_RECORD_FIELDS_IN_DECLARATION_ORDER).asString(input)); + } } + diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 22dd8642..477752df 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -13,7 +13,8 @@ Modules: 2.19.0 (not yet released) -- +#171: Add a `JSON.Feature.WRITE_RECORD_FIELDS_IN_DECLARATION_ORDER` for + retaining Serialization order of Java Records (instead of alphabetic) 2.18.1 (28-Oct-2024)