Java Records Support
Juneau supports serialization and parsing of Java records out of the box. Records are treated as beans whose
properties are derived from the record's components. The canonical constructor is automatically detected for
parsing, so no @BeanCtor annotation is needed.
Basic Usage
Records work seamlessly with all Juneau serializers and parsers:
public record Person(String name, int age) {}
// Serialization
Person person = new Person("John", 30);
String json = Json5Serializer.DEFAULT.write(person);
// Produces: {age:30,name:'John'}
// Parsing
Person parsed = JsonParser.DEFAULT.read("{\"name\":\"John\",\"age\":30}", Person.class);
How It Works
Juneau automatically detects Java records and handles them as follows:
- Property discovery: Record component accessor methods (e.g.
name()for anamecomponent) are recognized as getters, even though they don't follow the traditionalgetX()naming convention. - Constructor detection: The canonical constructor (the one whose parameters match the record components) is automatically identified. During parsing, property values are cached and then passed to the canonical constructor to create the record instance.
- Private fields: Record component fields are automatically included as bean properties despite being
private final.
Supported Features
Records work with the full range of Juneau's bean annotations and capabilities:
Property ordering with @BeanType(properties)
@BeanType(properties="age,name")
public record AnnotatedOrder(String name, int age) {}
Property renaming with @BeanProp
public record WithBeanProp(@BeanProp(name="fullName") String name, int age) {}
String json = Json5Serializer.DEFAULT.write(new WithBeanProp("John", 30));
// Produces: {age:30,fullName:'John'}
Non-canonical constructors with @BeanCtor
For records, the canonical constructor is automatically detected, so @BeanCtor is only needed if you want to use
a non-canonical constructor, for example to provide default values for certain components:
@BeanType(properties="name")
public record WithBeanCtor(String name, int age) {
@BeanCtor(properties="name")
public WithBeanCtor(String name) {
this(name, 0); // Default age to 0
}
}
When using @BeanCtor on a non-canonical constructor, use @BeanType(properties) to limit the visible properties to
match the constructor parameters.
Compact constructors
Records with compact constructors (validation/normalization logic) work as expected:
public record WithCompactConstructor(String name, int age) {
public WithCompactConstructor {
if (name == null) name = "unknown";
if (age < 0) age = 0;
}
}
Nested records, collections, and maps
public record Team(String name, List<Person> members) {}
public record Config(String id, Map<String,Object> settings) {}
public record Nested(Person person, String role) {}
Enums
public enum Priority { LOW, MEDIUM, HIGH }
public record WithEnum(String name, Priority priority) {}
Records implementing interfaces
public interface Identifiable {
String id();
}
public record IdentifiableRecord(String id, String data) implements Identifiable {}
Polymorphic records with @Marshalled(typeName)
@Marshalled(dictionary={DogRecord.class, CatRecord.class})
public interface AnimalRecord {}
@Marshalled(typeName="dog")
public record DogRecord(String name, String breed) implements AnimalRecord {}
@Marshalled(typeName="cat")
public record CatRecord(String name, int lives) implements AnimalRecord {}
Empty records
Records with no components are supported:
public record EmptyRecord() {}
String json = Json5Serializer.DEFAULT.write(new EmptyRecord());
// Produces: {}
Limitations
Because records are immutable and all components must be provided to the canonical constructor, the following operations are not supported during parsing:
@MarshalledIgnoreon individual record components -- Ignoring a component 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.@BeanType(excludeProperties)excluding record components -- Same limitation as@MarshalledIgnore.@BeanType(readOnlyProperties)/@BeanProp(ro)on record components -- Marking components as read-only will prevent them from being parsed, making the record un-parseable if the values are missing from the input.
These annotations will still affect serialization as expected. If you need to omit components during parsing,
use @BeanCtor with a non-canonical constructor that provides defaults for the omitted components.