Nesting, Containers, and Sealed Hierarchies
Whole mappings plug in wherever a leaf does, so structure composes and error paths compose with it.
Real DTOs are not flat. An order carries a customer, the customer carries an address, the order carries a list of lines, and the domain and wire sides are sealed hierarchies as often as they are single records. All of it maps with the machinery you already have: a nested spec is just a leaf, a container lifts its element's leaf or spec, and a sealed pair dispatches one spec per subtype pair.
- How specs in the same compilation nest automatically, composing failures into dotted paths
- How
List,Optional, andMapcomponents lift, and how failing elements are located by index or key - Why recursion terminates by construction
- Dispatching a mapping over two sealed interfaces, exhaustively in both directions
The code on this page is RecordMappingBook.java - the page includes it directly, so it is compiled and run by the build.
Nesting, containers, and recursion
A component whose two sides are themselves mapped by another spec in the same compilation nests automatically, and failures compose into dotted paths:
record Invoice(String id, Customer customer) {}
record InvoiceDto(String id, CustomerDto customer) {}
@GenerateMapping
interface InvoiceMapping extends MappingSpec<Invoice, InvoiceDto> {}
InvoiceMappingImpl.INSTANCE.parse(new InvoiceDto("INV-2", new CustomerDto("Bob", "nope")));
// Invalid(NonEmptyList[customer.email: not an email address])
Containers lift the same way:
ListandOptionalcomponents lift through the element's leaf or spec; each failing list element is located by its index, so a bad second element reports asemails.1(customers.1.emailthrough a nested spec).Mapcomponents lift their values; keys pass through untouched, and each entry's failures are located by its key, so a bad value under keyenreports asattributes.en.email.
Because nesting is delegation (each spec's Impl exposes asValidatedPrism(), so a whole mapping plugs in wherever a leaf does), recursion terminates by construction: a self-referential Tree(String value, List<Tree> children) maps with an empty spec and round-trips any finite tree.
The rendered path uses each key's toString(), so a key containing a dot looks the same as deeper nesting, and two distinct keys whose renderings collide share a location. The structured FieldError path list stays exact regardless, holding the whole key as one segment, and every error is still reported.
Sealed hierarchies
A MappingSpec over two sealed interfaces dispatches over the permitted subtype pairs, one spec per pair, exhaustively in both directions:
sealed interface Payment permits Card, Bank {}
record Card(String pan) implements Payment {}
record Bank(String iban) implements Payment {}
sealed interface PaymentDto permits CardDto, BankDto {}
record CardDto(String pan) implements PaymentDto {}
record BankDto(String iban) implements PaymentDto {}
@GenerateMapping
interface CardMapping extends MappingSpec<Card, CardDto> {}
@GenerateMapping
interface BankMapping extends MappingSpec<Bank, BankDto> {}
@GenerateMapping
interface PaymentMapping extends MappingSpec<Payment, PaymentDto> {}
// generated PaymentMappingImpl.build:
// return switch (domain) {
// case Card v -> CardMappingImpl.INSTANCE.build(v);
// case Bank v -> BankMappingImpl.INSTANCE.build(v);
// };
A domain subtype without a spec, or a wire subtype nothing produces, is a compile error: the dispatch cannot be partial.
- Nesting is delegation: any spec's Impl is a leaf (
asValidatedPrism()), so specs nest automatically and recursion terminates by construction - Containers lift:
List/Optionalby element,Mapby value; failures are located by index or key - Error paths are dotted domain names:
customers.1.email,attributes.en.email - Sealed dispatch is exhaustive both ways: a missing subtype pair is a compile error, never a runtime surprise
- Record Mapping Basics - The null doctrine that also reaches inside containers
- The Emission Tiers - What the composed mapping lawfully offers
- Generic Specs - Nesting for generic records
Previous: Standard Codecs and Shared Vocabulary Next: The Emission Tiers