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());
+ }
+}