This is an automated email from the ASF dual-hosted git repository.

jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git


The following commit(s) were added to refs/heads/master by this push:
     new c0c9396c8f Support for Java records
c0c9396c8f is described below

commit c0c9396c8fde7963c296487c6e36691533c11106
Author: James Bognar <[email protected]>
AuthorDate: Sat Feb 28 08:05:16 2026 -0500

    Support for Java records
---
 .../src/main/java/org/apache/juneau/BeanMeta.java  |  55 +++-
 .../java/org/apache/juneau/BeanPropertyMeta.java   |   2 +-
 .../java/org/apache/juneau/annotation/Bean.java    |  15 +
 .../org/apache/juneau/annotation/BeanIgnore.java   |   7 +
 .../java/org/apache/juneau/annotation/Beanc.java   |   7 +
 .../java/org/apache/juneau/annotation/Beanp.java   |   8 +
 .../juneau/a/rttests/Records_RoundTripTest.java    | 326 +++++++++++++++++++++
 7 files changed, 413 insertions(+), 7 deletions(-)

diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMeta.java 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMeta.java
index ea8abd76e9..3eb1ab5396 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMeta.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMeta.java
@@ -347,7 +347,7 @@ public class BeanMeta<T> {
                return new BeanMetaValue<>(null, reason);
        }
 
-       private final BeanConstructor beanConstructor;                          
   // The constructor for this bean.
+       private BeanConstructor beanConstructor;                                
   // The constructor for this bean.
        private final BeanContext beanContext;                                  
   // The bean context that created this metadata object.
        private final BeanFilter beanFilter;                                    
   // Optional bean filter associated with the target class.
        private final OptionalSupplier<InvocationHandler> 
beanProxyInvocationHandler;  // The invocation handler for this bean (if it's 
an interface).
@@ -421,8 +421,8 @@ public class BeanMeta<T> {
 
                this.typePropertyName = ba.stream().map(x -> 
x.inner().typePropertyName()).filter(Utils::ne).findFirst().orElseGet(beanContext::getBeanTypePropertyName);
 
-               // Check if constructor is required but not found
-               if (! beanConstructor.constructor().isPresent() && bf == null 
&& beanContext.isBeansRequireDefaultConstructor())
+               // Check if constructor is required but not found (records are 
exempt since they use canonical constructors)
+               if (! beanConstructor.constructor().isPresent() && bf == null 
&& beanContext.isBeansRequireDefaultConstructor() && ! ci.isRecord())
                        notABeanReasonTemp = "Class does not have the required 
no-arg constructor";
 
                var bfo = opt(bf);
@@ -512,6 +512,28 @@ public class BeanMeta<T> {
                        // Check for missing properties.
                        fixedBeanProps.stream().filter(x -> ! 
normalProps.containsKey(x)).findFirst().ifPresent(x -> { throw bex(c, "The 
property ''{0}'' was defined on the @Bean(properties=X) annotation of class 
''{1}'' but was not found on the class definition.", x, ci.getNameSimple()); });
 
+                       // For records with renamed properties, remap 
constructor args to use the actual property names.
+                       if (ci.isRecord() && ne(beanConstructor.args())) {
+                               var components = ci.getRecordComponents();
+                               var remappedArgs = new 
ArrayList<String>(beanConstructor.args().size());
+                               for (int idx = 0; idx < 
beanConstructor.args().size(); idx++) {
+                                       var componentName = 
beanConstructor.args().get(idx);
+                                       if 
(normalProps.containsKey(componentName)) {
+                                               remappedArgs.add(componentName);
+                                       } else {
+                                               var rcName = idx < 
components.size() ? components.get(idx).getName() : componentName;
+                                               var found = 
normalProps.entrySet().stream()
+                                                       .filter(e -> 
(e.getValue().field != null && e.getValue().field.hasName(rcName))
+                                                               || 
(e.getValue().getter != null && e.getValue().getter.hasName(rcName)))
+                                                       .map(Map.Entry::getKey)
+                                                       .findFirst()
+                                                       .orElse(componentName);
+                                               remappedArgs.add(found);
+                                       }
+                               }
+                               beanConstructor = new 
BeanConstructor(beanConstructor.constructor(), remappedArgs);
+                       }
+
                        // Mark constructor arg properties.
                        for (var fp : beanConstructor.args()) {
                                var m = normalProps.get(fp);
@@ -520,8 +542,8 @@ public class BeanMeta<T> {
                                m.setAsConstructorArg();
                        }
 
-                       // Make sure at least one property was found.
-                       if (bf == null && 
beanContext.isBeansRequireSomeProperties() && normalProps.isEmpty())
+                       // Make sure at least one property was found (records 
with no components are exempt).
+                       if (bf == null && 
beanContext.isBeansRequireSomeProperties() && normalProps.isEmpty() && ! 
ci.isRecord())
                                notABeanReasonTemp = "No properties detected on 
bean class";
 
                        sortPropertiesTemp = beanContext.isSortProperties() || 
bfo.map(x -> x.isSortProperties()).orElse(false) && fixedBeanProps.isEmpty();
@@ -992,6 +1014,14 @@ public class BeanMeta<T> {
                        return new BeanConstructor(opt(con), args);
                }
 
+               if (ci.isRecord()) {
+                       var components = ci.getRecordComponents();
+                       var paramTypes = 
components.stream().map(java.lang.reflect.RecordComponent::getType).toArray(Class[]::new);
+                       var rcon = ci.getPublicConstructor(x -> 
x.hasParameterTypes(paramTypes)).orElse(null);
+                       if (rcon != null)
+                               return new 
BeanConstructor(opt(rcon.accessible()), 
components.stream().map(java.lang.reflect.RecordComponent::getName).toList());
+               }
+
                if (implClassConstructor != null)
                        return new 
BeanConstructor(opt(implClassConstructor.accessible()), liste());
 
@@ -1023,6 +1053,10 @@ public class BeanMeta<T> {
                var v = beanContext.getBeanFieldVisibility();
                var noIgnoreTransients = ! 
beanContext.isIgnoreTransientFields();
                var ap = beanContext.getAnnotationProvider();
+               var isRecord = classMeta.isRecord();
+               var recordComponentNames = isRecord
+                       ? 
classMeta.getRecordComponents().stream().map(java.lang.reflect.RecordComponent::getName).collect(java.util.stream.Collectors.toSet())
+                       : Set.<String>of();
                // @formatter:off
                return classHierarchy.get().stream()
                        .flatMap(c2 -> c2.getDeclaredFields().stream())
@@ -1030,7 +1064,8 @@ public class BeanMeta<T> {
                                && (x.isNotTransient() || noIgnoreTransients)
                                && (! x.hasAnnotation(Transient.class) || 
noIgnoreTransients)
                                && ! ap.has(BeanIgnore.class, x)
-                               && (v.isVisible(x.inner()) || 
ap.has(Beanp.class, x)))
+                               && (v.isVisible(x.inner()) || 
ap.has(Beanp.class, x)
+                                       || (isRecord && 
recordComponentNames.contains(x.getName()))))
                        .toList();
                // @formatter:on
        }
@@ -1122,6 +1157,8 @@ public class BeanMeta<T> {
                                        } else if (n.startsWith("is") && 
(rt.is(Boolean.TYPE) || rt.is(Boolean.class))) {
                                                methodType = GETTER;
                                                n = n.substring(2);
+                                       } else if (ci.isRecord() && 
isRecordAccessor(m, ci)) {
+                                               methodType = GETTER;
                                        } else if (nn(bpName)) {
                                                methodType = GETTER;
                                                if (bpName.isEmpty()) {
@@ -1185,6 +1222,12 @@ public class BeanMeta<T> {
                return l;
        }
 
+       private static boolean isRecordAccessor(MethodInfo m, ClassInfo ci) {
+               return ci.getRecordComponents().stream()
+                       .anyMatch(rc -> rc.getName().equals(m.getNameSimple())
+                               && 
rc.getType().equals(m.getReturnType().inner()));
+       }
+
        /*
         * Creates a bean registry for this bean class.
         *
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanPropertyMeta.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanPropertyMeta.java
index 18ca9c33a8..3828d5e563 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanPropertyMeta.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanPropertyMeta.java
@@ -313,7 +313,7 @@ public class BeanPropertyMeta implements 
Comparable<BeanPropertyMeta> {
                                return false;
 
                        canRead |= (nn(field) || nn(getter));
-                       canWrite |= (nn(field) || nn(setter));
+                       canWrite |= (nn(field) || nn(setter) || 
isConstructorArg);
 
                        var ifi = innerField;
                        var gi = getter;
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/Bean.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/Bean.java
index a9385210a9..c6270a604b 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/Bean.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/Bean.java
@@ -132,6 +132,11 @@ public @interface Bean {
         * <h5 class='section'>Notes:</h5><ul>
         *      <li class='note'>
         *              {@link #xp()} is a shortened synonym for this value.
+        *      <li class='note'>
+        *              <b>Java Records:</b> Excluding record components is not 
supported during parsing.
+        *              Because records are immutable, all components must be 
provided to the canonical constructor.
+        *              Excluded components will be omitted from serialization 
output, but the parser will be unable to
+        *              instantiate the record if the excluded component values 
are missing from the input.
         * </ul>
         *
         * <h5 class='section'>See Also:</h5><ul>
@@ -330,6 +335,11 @@ public @interface Bean {
         * <h5 class='section'>Notes:</h5><ul>
         *      <li class='note'>
         *              {@link #p()} is a shortened synonym for this value.
+        *      <li class='note'>
+        *              <b>Java Records:</b> When used on records to control 
property order, all record components should
+        *              be included.  Omitting record components from this list 
will prevent them from being parsed, and
+        *              because records are immutable, all components must be 
provided to the canonical constructor.
+        *              Use {@link Beanc @Beanc} to specify a non-canonical 
constructor if you need to omit components.
         * </ul>
         *
         * <h5 class='section'>See Also:</h5><ul>
@@ -380,6 +390,11 @@ public @interface Bean {
         * <h5 class='section'>Notes:</h5><ul>
         *      <li class='note'>
         *              {@link #ro()} is a shortened synonym for this value.
+        *      <li class='note'>
+        *              <b>Java Records:</b> Marking record components as 
read-only is not supported during parsing.
+        *              Because records are immutable, all components must be 
provided to the canonical constructor.
+        *              Read-only components will be serialized as usual, but 
the parser will be unable to instantiate the
+        *              record if the read-only component values are missing 
from the input.
         * </ul>
         *
         * <h5 class='section'>See Also:</h5><ul>
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/BeanIgnore.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/BeanIgnore.java
index 42a12bf1bb..f3890b070c 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/BeanIgnore.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/BeanIgnore.java
@@ -33,6 +33,13 @@ import java.lang.annotation.*;
  *     <li><ja>@Rest</ja>-annotated classes and <ja>@RestOp</ja>-annotated 
methods when an {@link #on()} value is specified.
  * </ul>
  *
+ * <h5 class='section'>Java Records:</h5>
+ * <p>
+ * Ignoring individual record components is not supported during parsing.
+ * Because records are immutable, all components must be provided to the 
canonical constructor.
+ * Applying this annotation to a record component's accessor method or field 
will exclude it from serialization
+ * output, but the parser will be unable to instantiate the record if the 
component value is missing from the input.
+ *
  * <h5 class='section'>See Also:</h5><ul>
  *     <li class='link'><a class="doclink" 
href="https://juneau.apache.org/docs/topics/BeanIgnoreAnnotation";>@BeanIgnore 
Annotation</a>
 
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/Beanc.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/Beanc.java
index e7e8017788..30e0cab2d1 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/Beanc.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/Beanc.java
@@ -42,6 +42,13 @@ import org.apache.juneau.*;
  * <p>
  * This annotation can only be applied to constructors and can only be applied 
to one constructor per class.
  *
+ * <h5 class='section'>Java Records:</h5>
+ * <p>
+ * For Java records, the canonical constructor and its property mappings are 
automatically detected, so this
+ * annotation is not required.  It can still be used to specify a 
non-canonical constructor if needed, for example
+ * to provide default values for certain components.  When using a 
non-canonical constructor on a record, use
+ * {@link Bean#properties() @Bean(properties)} to limit the visible properties 
to match the constructor parameters.
+ *
  * <p>
  * When present, bean instantiation is delayed until the call to {@link 
BeanMap#getBean()}.
  * Until then, bean property values are stored in a local cache until 
<c>getBean()</c> is called.
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/Beanp.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/Beanp.java
index deaa9eadcd..6b7b46a576 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/Beanp.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/Beanp.java
@@ -392,6 +392,14 @@ public @interface Beanp {
         *      }
         * </p>
         *
+        * <h5 class='section'>Notes:</h5><ul>
+        *      <li class='note'>
+        *              <b>Java Records:</b> Marking record components as 
read-only is not supported during parsing.
+        *              Because records are immutable, all components must be 
provided to the canonical constructor.
+        *              Read-only components will be serialized as usual, but 
the parser will be unable to instantiate the
+        *              record if the read-only component values are missing 
from the input.
+        * </ul>
+        *
         * <h5 class='section'>See Also:</h5><ul>
         *      <li class='jm'>{@link 
org.apache.juneau.BeanContext.Builder#beanPropertiesReadOnly(Class, String)}
         *      <li class='jm'>{@link 
org.apache.juneau.BeanContext.Builder#beanPropertiesReadOnly(String, String)}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/Records_RoundTripTest.java
 
b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/Records_RoundTripTest.java
new file mode 100644
index 0000000000..48a3a078e3
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/Records_RoundTripTest.java
@@ -0,0 +1,326 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.a.rttests;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.*;
+
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.json.*;
+import org.junit.jupiter.api.*;
+import org.junit.jupiter.params.*;
+import org.junit.jupiter.params.provider.*;
+
+class Records_RoundTripTest extends RoundTripTest_Base {
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // Record definitions
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       public record Person(String name, int age) {}
+
+       public record Team(String name, List<Person> members) {}
+
+       public record Config(String id, Map<String,Object> settings) {}
+
+       public record NullableRecord(String name, String nickname) {}
+
+       public record EmptyRecord() {}
+
+       public record WithArray(String label, int[] values) {}
+
+       public enum Priority { LOW, MEDIUM, HIGH }
+
+       public record WithEnum(String name, Priority priority) {}
+
+       @Bean(properties="age,name")
+       public record AnnotatedOrder(String name, int age) {}
+
+       public record WithBeanp(@Beanp(name="fullName") String name, int age) {}
+
+       public record WithCompactConstructor(String name, int age) {
+               public WithCompactConstructor {
+                       if (name == null) name = "unknown";
+                       if (age < 0) age = 0;
+               }
+       }
+
+       public record Nested(Person person, String role) {}
+
+       public record WithNullValues(String required, String optional) {}
+
+       @Bean(properties="name")
+       public record WithBeanc(String name, int age) {
+               @Beanc(properties="name")
+               public WithBeanc(String name) {
+                       this(name, 0);
+               }
+       }
+
+       public record Wrapper<T>(T value, String label) {}
+
+       public interface Identifiable {
+               String id();
+       }
+
+       public record IdentifiableRecord(String id, String data) implements 
Identifiable {}
+
+       @Bean(dictionary={DogRecord.class, CatRecord.class})
+       public interface AnimalRecord {}
+
+       @Bean(typeName="dog")
+       public record DogRecord(String name, String breed) implements 
AnimalRecord {}
+
+       @Bean(typeName="cat")
+       public record CatRecord(String name, int lives) implements AnimalRecord 
{}
+
+       public record AnimalHolder(AnimalRecord animal) {}
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // Basic round-trip tests
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @ParameterizedTest
+       @MethodSource("testers")
+       void a01_simpleRecord(RoundTrip_Tester t) throws Exception {
+               var in = new Person("John", 30);
+               var out = t.roundTrip(in, Person.class);
+               assertEquals("John", out.name());
+               assertEquals(30, out.age());
+       }
+
+       @ParameterizedTest
+       @MethodSource("testers")
+       void a02_nestedRecord(RoundTrip_Tester t) throws Exception {
+               var person = new Person("Jane", 25);
+               var in = new Nested(person, "developer");
+               var out = t.roundTrip(in, Nested.class);
+               assertEquals("Jane", out.person().name());
+               assertEquals(25, out.person().age());
+               assertEquals("developer", out.role());
+       }
+
+       @ParameterizedTest
+       @MethodSource("testers")
+       void a03_recordWithList(RoundTrip_Tester t) throws Exception {
+               var members = List.of(new Person("Alice", 30), new 
Person("Bob", 25));
+               var in = new Team("devs", members);
+               var out = t.roundTrip(in, Team.class);
+               assertEquals("devs", out.name());
+               assertEquals(2, out.members().size());
+               assertEquals("Alice", out.members().get(0).name());
+               assertEquals("Bob", out.members().get(1).name());
+       }
+
+       @ParameterizedTest
+       @MethodSource("testers")
+       void a04_recordWithMap(RoundTrip_Tester t) throws Exception {
+               var settings = Map.<String,Object>of("key1", "val1", "key2", 
42);
+               var in = new Config("cfg1", settings);
+               var out = t.roundTrip(in, Config.class);
+               assertEquals("cfg1", out.id());
+               assertEquals("val1", out.settings().get("key1"));
+       }
+
+       @ParameterizedTest
+       @MethodSource("testers")
+       void a05_recordWithNulls(RoundTrip_Tester t) throws Exception {
+               var in = new NullableRecord("John", null);
+               var out = t.roundTrip(in, NullableRecord.class);
+               assertEquals("John", out.name());
+               assertNull(out.nickname());
+       }
+
+       @ParameterizedTest
+       @MethodSource("testers")
+       void a06_emptyRecord(RoundTrip_Tester t) throws Exception {
+               var in = new EmptyRecord();
+               var out = t.roundTrip(in, EmptyRecord.class);
+               assertNotNull(out);
+       }
+
+       @ParameterizedTest
+       @MethodSource("testers")
+       void a07_recordWithEnum(RoundTrip_Tester t) throws Exception {
+               var in = new WithEnum("task1", Priority.HIGH);
+               var out = t.roundTrip(in, WithEnum.class);
+               assertEquals("task1", out.name());
+               assertEquals(Priority.HIGH, out.priority());
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // Annotation integration tests
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @ParameterizedTest
+       @MethodSource("testers")
+       void b01_beanPropertyOrder(RoundTrip_Tester t) throws Exception {
+               var in = new AnnotatedOrder("John", 30);
+               var out = t.roundTrip(in, AnnotatedOrder.class);
+               assertEquals("John", out.name());
+               assertEquals(30, out.age());
+       }
+
+       @ParameterizedTest
+       @MethodSource("testers")
+       void b02_beanpRename(RoundTrip_Tester t) throws Exception {
+               var in = new WithBeanp("John", 30);
+               var out = t.roundTrip(in, WithBeanp.class);
+               assertEquals("John", out.name());
+               assertEquals(30, out.age());
+       }
+
+       @ParameterizedTest
+       @MethodSource("testers")
+       void b03_beancCustomConstructor(RoundTrip_Tester t) throws Exception {
+               var in = new WithBeanc("John", 0);
+               var out = t.roundTrip(in, WithBeanc.class);
+               assertEquals("John", out.name());
+               assertEquals(0, out.age());
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // Type complexity tests
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @Test
+       void b05_genericRecord() throws Exception {
+               var in = new Wrapper<>("hello", "greeting");
+               var json = Json5Serializer.DEFAULT.serialize(in);
+               assertTrue(json.contains("hello"));
+               assertTrue(json.contains("greeting"));
+       }
+
+       @Test
+       void b06_recordWithArraySerialization() throws Exception {
+               var in = new WithArray("data", new int[]{1, 2, 3});
+               var json = Json5Serializer.DEFAULT.serialize(in);
+               assertEquals("{label:'data',values:[1,2,3]}", json);
+       }
+
+       @ParameterizedTest
+       @MethodSource("testers")
+       void b07_recordImplementingInterface(RoundTrip_Tester t) throws 
Exception {
+               var in = new IdentifiableRecord("id1", "some data");
+               var out = t.roundTrip(in, IdentifiableRecord.class);
+               assertEquals("id1", out.id());
+               assertEquals("some data", out.data());
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // Edge case tests
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @ParameterizedTest
+       @MethodSource("testers")
+       void c01_compactConstructor(RoundTrip_Tester t) throws Exception {
+               var in = new WithCompactConstructor("John", 30);
+               var out = t.roundTrip(in, WithCompactConstructor.class);
+               assertEquals("John", out.name());
+               assertEquals(30, out.age());
+       }
+
+       @ParameterizedTest
+       @MethodSource("testers")
+       void c02_recordInCollection(RoundTrip_Tester t) throws Exception {
+               var in = List.of(new Person("Alice", 30), new Person("Bob", 
25));
+               var out = t.roundTrip(in);
+               assertNotNull(out);
+       }
+
+       @Test
+       void c03_recordAsMapValue() throws Exception {
+               var map = Map.of("p1", new Person("Alice", 30), "p2", new 
Person("Bob", 25));
+               var json = Json5Serializer.DEFAULT.serialize(map);
+               assertTrue(json.contains("Alice"));
+               assertTrue(json.contains("Bob"));
+       }
+
+       @Test
+       void c04_beancNonCanonicalConstructor() throws Exception {
+               var in = new WithBeanc("Jane", 0);
+               var json = Json5Serializer.DEFAULT.serialize(in);
+               assertTrue(json.contains("Jane"));
+               var out = JsonParser.DEFAULT.parse("{\"name\":\"Jane\"}", 
WithBeanc.class);
+               assertEquals("Jane", out.name());
+               assertEquals(0, out.age());
+       }
+
+       @Test
+       void c05_polymorphicRecordWithTypeName() throws Exception {
+               var dog = new DogRecord("Rex", "Labrador");
+               var s = 
Json5Serializer.DEFAULT.copy().addBeanTypes().addRootType().build();
+               var json = s.serialize(dog);
+               assertTrue(json.contains("dog"));
+               assertTrue(json.contains("Rex"));
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // JSON-specific serialization tests
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @Test
+       void d01_jsonSerialization() throws Exception {
+               var p = new Person("John", 30);
+               var json = Json5Serializer.DEFAULT.serialize(p);
+               assertEquals("{age:30,name:'John'}", json);
+       }
+
+       @Test
+       void d02_jsonSerializationNested() throws Exception {
+               var person = new Person("Jane", 25);
+               var nested = new Nested(person, "developer");
+               var json = Json5Serializer.DEFAULT.serialize(nested);
+               assertEquals("{person:{age:25,name:'Jane'},role:'developer'}", 
json);
+       }
+
+       @Test
+       void d03_jsonSerializationEmpty() throws Exception {
+               var empty = new EmptyRecord();
+               var json = Json5Serializer.DEFAULT.serialize(empty);
+               assertEquals("{}", json);
+       }
+
+       @Test
+       void d04_jsonSerializationWithEnum() throws Exception {
+               var status = new WithEnum("task1", Priority.HIGH);
+               var json = Json5Serializer.DEFAULT.serialize(status);
+               assertEquals("{name:'task1',priority:'HIGH'}", json);
+       }
+
+       @Test
+       void d05_jsonSerializationBeanpRename() throws Exception {
+               var p = new WithBeanp("John", 30);
+               var json = Json5Serializer.DEFAULT.serialize(p);
+               assertEquals("{age:30,fullName:'John'}", json);
+       }
+
+       @Test
+       void d06_jsonSerializationPropertyOrder() throws Exception {
+               var p = new AnnotatedOrder("John", 30);
+               var json = Json5Serializer.DEFAULT.serialize(p);
+               assertEquals("{age:30,name:'John'}", json);
+       }
+
+       @Test
+       void d07_jsonParsingRenamedProperty() throws Exception {
+               var parsed = 
JsonParser.DEFAULT.parse("{\"fullName\":\"John\",\"age\":30}", WithBeanp.class);
+               assertEquals("John", parsed.name());
+               assertEquals(30, parsed.age());
+       }
+}

Reply via email to