Skip to main content

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 a name component) are recognized as getters, even though they don't follow the traditional getX() 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:

  • @MarshalledIgnore on 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.