# Nesting, Containers, and Sealed Hierarchies

_A record that holds records, lists of them, or a sealed type maps through the specs you already write, and every failure names its full path._

Real DTOs are not flat: an order carries a customer and a list of lines, and a payment is a card or a bank transfer. This page walks those three first, a nested spec, a list of them and a sealed dispatch, all through machinery you already have. Optional objects, other containers, map keys, flattening and specs from other modules follow, for when a pair needs them.

~~~admonish info title="What You'll Learn"
- Nest a spec in another, in a `List`, or behind a sealed dispatch, and predict the path each failure reports
- Predict what happens to a mapped `List` after `parse` or `build` hands it over
~~~

~~~admonish example title="See Example Code"
**The code on this page is [StructureBook.java](https://github.com/higher-kinded-j/higher-kinded-j/blob/main/hkj-examples/src/main/java/org/higherkindedj/example/book/mapping/StructureBook.java) and its [StructureBookTest.java](https://github.com/higher-kinded-j/higher-kinded-j/blob/main/hkj-examples/src/test/java/org/higherkindedj/example/book/mapping/StructureBookTest.java)** - the page includes them directly, so they are compiled and run by the build.
~~~

## Nesting a spec, and a list of them {#nesting-containers-and-recursion}

When a component's two types are a pair another spec maps, the outer spec uses that spec. `CustomerMapping`, from [Validated leaves](basics.md#validated-leaves), maps `Customer` and `CustomerDto`, so `InvoiceMapping` stays empty, and a failure inside comes back under the component's name. Forget the inner spec, and the processor refuses the outer one with [`has no usable source`](compiler_errors.md#no-usable-source), offering a spec.

``` java
record Invoice(String id, Customer customer) {}

record InvoiceDto(String id, CustomerDto customer) {}

@GenerateMapping
interface InvoiceMapping extends MappingSpec<Invoice, InvoiceDto> {}


    Validated<NonEmptyList<FieldError>, Invoice> invoice =
        InvoiceMappingImpl.INSTANCE.parse(new InvoiceDto("INV-2", new CustomerDto("Bob", "nope")));
    // Invalid(NonEmptyList[customer.email: not an email address])
```

A `List` of such pairs **lifts** the element's spec, which means it parses every element through it. Think `dtos.stream().map(mapper::toDomain).toList()`, except that a bad element does not stop the others, and each failure keeps its index. Here the second line's price fails its leaf:

``` java
record CartLine(String sku, BigDecimal price) {}

record CartLineDto(String sku, String price) {}

record Cart(String id, List<CartLine> lines) {}

record CartDto(String id, List<CartLineDto> lines) {}

@GenerateMapping
interface CartLineMapping extends MappingSpec<CartLine, CartLineDto> {
  default ValidatedPrism<String, BigDecimal> price() {
    return StandardCodecs.bigDecimal();
  }
}

@GenerateMapping
interface CartMapping extends MappingSpec<Cart, CartDto> {} // lines: CartLineMapping, per element


    Validated<NonEmptyList<FieldError>, Cart> cart =
        CartMappingImpl.INSTANCE.parse(
            new CartDto(
                "C-1",
                List.of(new CartLineDto("SKU-1", "9.99"), new CartLineDto("SKU-2", "12,50"))));
    // Invalid(NonEmptyList[lines.1.price: not a number in plain notation (expected e.g. 123.45)])
```

That is the `lines.1.price` error from the [chapter's opening response](ch_intro.md#what-you-get-instead):

```mermaid
%%{init: {"sequence": {"actorMargin": 20, "diagramMarginX": 10}}}%%
sequenceDiagram
    accTitle: How an error path grows outward
    accDescr: The price leaf reports a bare message. CartLineMapping puts price in front of it, the list puts the index 1 in front of that, and CartMapping puts lines in front, so the client reads lines.1.price.
    participant C as CartMapping
    participant L as lines list
    participant I as CartLineMapping
    participant P as price leaf
    C->>L: parse lines
    L->>I: parse index 1
    I->>P: parse "12,50"
    P-->>I: not a number...
    I-->>L: price: not a number...
    L-->>C: 1.price: not a number...
    Note over C: lines.1.price
```

In words: the leaf reports only its message, and on the way out each level puts its own name in front, first `price`, then the index `1`, then `lines`.

Indexes count from zero, so `lines.1` is the second line. An array lifts exactly as a `List` does, and a `null` element is reported at its index, under the [null rule](basics.md#null-doctrine). Lifting needs the same container on both sides: [What lifts, and what does not](rules.md#what-lifts) has the exact rule.

~~~admonish warning title="Not checked for you: a mapped `List`, `Set` or `Map` is unmodifiable"
A component declared as a `List`, `Set` or `Map` comes out of `parse` and `build` unmodifiable, whether its elements went through a spec, like `lines`, or needed no conversion, like a `List<String> tags`. Adding to one throws `UnsupportedOperationException`, so copy it first. One that needs no conversion is copied both ways, so neither side holds the other's list. A component declared as `ArrayList`, or any other subtype, is shared as it is. [Same-typed containers cross as copies](rules.md#same-typed-containers-cross-as-copies) has the precise rule.
~~~

---

## Sealed hierarchies {#sealed-hierarchies}

A `MappingSpec` over two **sealed interfaces** dispatches over the permitted subtypes, one spec per subtype pair, in both directions:

``` java
sealed interface Payment permits Card, Bank {}

record Card(String pan) implements Payment {}

record Bank(String iban) implements Payment {}

// Jackson binds the wire before parse runs, so the wire says how to tell its subtypes apart.
@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION) // by their fields: pan or iban
@JsonSubTypes({@JsonSubTypes.Type(CardDto.class), @JsonSubTypes.Type(BankDto.class)})
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);
//   };
```

The dispatch hands each value to its subtype's spec and adds nothing to the path, since the JSON has no key for the subtype:

``` java
    PaymentMappingImpl paymentMapping = PaymentMappingImpl.INSTANCE;

    PaymentDto bankWire = paymentMapping.build(new Bank("GB33BUKB20201555555555"));
    // BankDto[iban=GB33BUKB20201555555555]
    Validated<NonEmptyList<FieldError>, Payment> card =
        paymentMapping.parse(new CardDto("4111111111111111"));
    // Valid(Card[pan=4111111111111111])
```

A leaf for `pan` goes on `CardMapping`, because a sealed spec has no components to bind one to ([`has no meaning on a sealed mapping`](compiler_errors.md#no-meaning-on-a-sealed-mapping)). The dispatch cannot be partial. The processor names a missing subtype, with [`has no mapping spec`](compiler_errors.md#subtype-has-no-spec) on the domain side and [`is never produced`](compiler_errors.md#subtype-never-produced) on the wire. A subtype must be a record or a sealed interface, or on the wire a bean as well. A generic subtype, an enum or any other class is not supported yet.

~~~admonish warning title="Not checked for you: Jackson must be told how to tell the subtypes apart"
Jackson binds the request before `parse` runs, and cannot construct an interface. Without type information, Jackson fails with a type-definition error, which Spring maps to no status by default. The exception escapes unhandled, and the server answers 500 with no field path. Annotate the wire interface as `PaymentDto` is: `@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION)` with `@JsonSubTypes` naming each record, when each subtype's fields differ. Where they overlap, use `Id.NAME` with a `type` property. An empty subtype changes what `{}` means:

``` java
sealed interface Fulfilment permits Shipped, Collected {}

record Shipped(String tracking) implements Fulfilment {}

record Collected() implements Fulfilment {} // collected in store: nothing to carry

@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION) // by their fields, and {} is CollectedDto
@JsonSubTypes({@JsonSubTypes.Type(ShippedDto.class), @JsonSubTypes.Type(CollectedDto.class)})
sealed interface FulfilmentDto permits ShippedDto, CollectedDto {}

record ShippedDto(String tracking) implements FulfilmentDto {}

record CollectedDto() implements FulfilmentDto {}

@GenerateMapping
interface ShippedMapping extends MappingSpec<Shipped, ShippedDto> {}

@GenerateMapping
interface CollectedMapping extends MappingSpec<Collected, CollectedDto> {}

@GenerateMapping
interface FulfilmentMapping extends MappingSpec<Fulfilment, FulfilmentDto> {}

```

`DEDUCTION` binds `{}` as `CollectedDto`, so a shipment missing its `tracking` parses as `Collected`, silently. `Id.NAME` avoids that.
~~~

~~~admonish tip title="You can ship now"
You can now map records that hold other records, lists of them, and sealed hierarchies, with every failure located by its full path. A nested object the client may leave out takes [`@OptionalBridge`](#optional-nested-objects). The rest of this page, [other containers](#other-containers), [map keys](#converting-map-keys), [flattening](#flattening-a-nested-component-onto-a-flat-wire) and [specs from other modules](#across-modules), is for when a pair needs them.
~~~

~~~admonish question title="Checkpoint: where does each failure locate?" id="check-structure-paths"
`CheckoutMapping` maps a `List` of the sealed payments:

``` java
record Checkout(String id, List<Payment> payments) {}

record CheckoutDto(String id, List<PaymentDto> payments) {}

@GenerateMapping
interface CheckoutMapping extends MappingSpec<Checkout, CheckoutDto> {}

```

A client sends three payments: a card with no card number (`pan`), a valid bank transfer, and a bank transfer with no IBAN. What does `CheckoutMappingImpl.INSTANCE.parse` report?

1. `payments.0.pan: must not be null` only, since the list stops at its first bad element
2. `payments.1.pan` and `payments.3.iban`, each `must not be null`
3. `payments.0.Card.pan` and `payments.2.Bank.iban`, each `must not be null`
4. `payments.0.pan` and `payments.2.iban`, each `must not be null`
~~~

~~~admonish success title="Answer and why" collapsible=true id="check-structure-paths-answer"
**4.** A bad element does not stop the others, and indexes count from zero, so the first and third payments are `0` and `2`. The dispatch adds no segment, so each path goes straight from the index to the field:

``` java
    assertThatValidated(
            CheckoutMappingImpl.INSTANCE.parse(
                new CheckoutDto(
                    "C-1",
                    List.of(
                        new CardDto(null),
                        new BankDto("GB33BUKB20201555555555"),
                        new BankDto(null)))))
        .hasFieldErrors("payments.0.pan: must not be null", "payments.2.iban: must not be null");
```

Where this lives: [Nesting a spec, and a list of them](#nesting-containers-and-recursion), [Sealed hierarchies](#sealed-hierarchies) and [Null has an address](basics.md#null-doctrine).
~~~

~~~admonish question title="Checkpoint: whose list is it?" id="check-structure-copy"
`OrderNote` and `OrderNoteDto` both hold a `List<String> tags`, and `OrderNoteMapping` is empty:

``` java
record OrderNote(String text, List<String> tags) {}

record OrderNoteDto(String text, List<String> tags) {}

@GenerateMapping
interface OrderNoteMapping extends MappingSpec<OrderNote, OrderNoteDto> {}

```

A controller parses an `OrderNoteDto` whose `tags` is the `ArrayList` Jackson bound, holding `"vip"`. A later filter clears that `ArrayList`. What does the parsed note's `tags()` hold then, and what does `note.tags().add("urgent")` do?

1. Nothing, and the add succeeds
2. `"vip"`, and the add succeeds
3. `"vip"`, and the add throws `UnsupportedOperationException`
4. Nothing, and the add throws `UnsupportedOperationException`
~~~

~~~admonish success title="Answer and why" collapsible=true id="check-structure-copy-answer"
**3.** `tags` is declared `List` and needs no conversion, so `parse` copies it, and the copy is unmodifiable. Clearing the request's list cannot reach the note, and adding to the note's list throws:

``` java
    List<String> requestTags = new ArrayList<>(List.of("vip")); // what Jackson bound
    OrderNote note =
        OrderNoteMappingImpl.INSTANCE.parse(new OrderNoteDto("Call back", requestTags)).get();
    requestTags.clear(); // a later filter clears the request's list

    assertThat(note.tags()).containsExactly("vip"); // a copy: still there
    assertThatThrownBy(() -> note.tags().add("urgent"))
        .isInstanceOf(UnsupportedOperationException.class); // and unmodifiable
```

Build a new `OrderNote` with the tags you want, or copy them first with `new ArrayList<>(note.tags())`.

Where this lives: [Nesting a spec, and a list of them](#nesting-containers-and-recursion).
~~~

---

## Optional nested objects {#optional-nested-objects}

When a client leaves an object out, or sends `null` for it, the wire holds a nullable `CustomerDto` where the domain holds an `Optional<Customer>`. That is the [`@OptionalBridge`](absence.md#optional-bridge) shape, and when a spec maps the pair, the marker is all the component needs:

``` java
record Referral(String code, Optional<Customer> referrer) {}

// A client with no referrer leaves the object out, which the JSON binder reads as null.
record ReferralDto(String code, @Nullable CustomerDto referrer) {}

@GenerateMapping
interface ReferralMapping extends MappingSpec<Referral, ReferralDto> {
  // CustomerMapping maps the element pair, so the marker is all this component needs.
  @OptionalBridge
  Optional<Customer> referrer();
}


    ReferralMappingImpl referralMapping = ReferralMappingImpl.INSTANCE;

    Validated<NonEmptyList<FieldError>, Referral> noReferrer =
        referralMapping.parse(new ReferralDto("R-7", null));
    // Valid(Referral[code=R-7, referrer=Optional.empty])

    Validated<NonEmptyList<FieldError>, Referral> badReferrer =
        referralMapping.parse(new ReferralDto("R-7", new CustomerDto("Bob", "nope")));
    // Invalid(NonEmptyList[referrer.email: not an email address])
```

`parse` reads `null` as empty, and hands a present value to the nested spec, so its failures locate under the component. `build` writes the nested build of a present value, and `null` for an empty one.

A bridged list lifts the same way. Where an absent list and an empty one mean different things, the domain holds an `Optional<List<Customer>>`, and the marker is again all it needs:

``` java
// A promotion open to everyone comes with no invitee list, which is not an empty one: an empty
// list invites no one. So the domain keeps the difference.
record Promotion(String name, Optional<List<Customer>> invitees) {}

// A client that sends no list leaves the array out, which the JSON binder reads as null.
record PromotionDto(String name, @Nullable List<CustomerDto> invitees) {}

@GenerateMapping
interface PromotionMapping extends MappingSpec<Promotion, PromotionDto> {
  // CustomerMapping maps the elements, so the marker is all the list needs.
  @OptionalBridge
  Optional<List<Customer>> invitees();
}


    PromotionMappingImpl promotionMapping = PromotionMappingImpl.INSTANCE;

    Validated<NonEmptyList<FieldError>, Promotion> noInvitees =
        promotionMapping.parse(new PromotionDto("Spring launch", null));
    // Valid(Promotion[name=Spring launch, invitees=Optional.empty])

    Validated<NonEmptyList<FieldError>, Promotion> badInvitee =
        promotionMapping.parse(
            new PromotionDto(
                "Spring launch",
                List.of(
                    new CustomerDto("Ada", "ada@example.org"), new CustomerDto("Bob", "nope"))));
    // Invalid(NonEmptyList[invitees.1.email: not an email address])
```

An empty list parses to a present, empty `Optional`, and [What converts a bridged value](rules.md#what-converts-a-bridged-value) covers every bridged container. Where an empty list already says there are none, a plain `List<Customer>` is simpler.

---

## Other containers, and recursion {#other-containers}

A `Set`, an array, an `Optional` and a `Map` lift like a `List`. Each locates a failure by whatever identifies an element in that container:

| Component | Lifts through the element's leaf or spec | A failure locates by |
| --- | --- | --- |
| `List<E>` | ✅ | its **index**: `lines.1.price` through a nested spec |
| `E[]` | ✅ | its **index**, exactly as a list: `standby.1` |
| `Set<E>` | ✅ | the **element's own rendering** (`agents.nope`), since a set has no index |
| `Optional<E>` on both sides | ✅ | the component itself, since there is only one element |
| `Map<K, V>` | ✅ values, and keys with [`@MapKey`](#converting-map-keys) | the **source key**: `notes.bad-key` |

``` java
// The support desk: its agents, those on standby in turn, and a note per agent.
record SupportDesk(
    Set<EmailAddress> agents, // a Set lifts like a List
    EmailAddress[] standby, // so does an array
    Map<EmailAddress, String> notes) {} // and a Map's KEYS, with @MapKey

record SupportDeskDto(Set<String> agents, String[] standby, Map<String, String> notes) {}

@GenerateMapping
interface SupportDeskMapping extends MappingSpec<SupportDesk, SupportDeskDto> {
  default ValidatedPrism<String, EmailAddress> agents() {
    return EmailCodecs.EMAIL;
  }

  default ValidatedPrism<String, EmailAddress> standby() {
    return EmailCodecs.EMAIL;
  }

  // A value leaf is named after its component; a key leaf is named BY its annotation,
  // because the two cannot share the one name Java allows.
  @MapKey("notes")
  default ValidatedPrism<String, EmailAddress> noteKey() {
    return EmailCodecs.EMAIL;
  }
}


    Validated<NonEmptyList<FieldError>, SupportDesk> desk =
        SupportDeskMappingImpl.INSTANCE.parse(
            new SupportDeskDto(
                Set.of("nope"),
                new String[] {"ada@example.org", "also-nope"},
                Map.of("bad-key", "a note")));
    // Invalid(NonEmptyList[
    //   agents.nope: not an email address,    <- a Set locates by the element itself
    //   standby.1: not an email address,      <- an array locates by index
    //   notes.bad-key: not an email address]) <- a key locates by the key it was sent as
```

A domain `Optional` against a plain nullable wire component is the [bridge](#optional-nested-objects) instead. A set has no index or reliable order, so the path names the element by its value. Keys and set elements render by `toString()`, so a dot inside one reads like deeper nesting, and two that render alike share a location, though every error is still reported. `FieldError.path()`, the `segments` array in a 422, keeps each as one segment.

An array that needs no conversion crosses as a clone. A record compares an array by reference, so a record with an array and no `equals` of its own is not equal to its own round trip.

The generated Impl calls a nested spec as it would a leaf, through its `asValidatedPrism()`, so recursion needs nothing special: a shop menu's `MenuSection(String name, List<MenuSection> children)` maps with an empty spec. A [one-directional bean mapping](beans.md#one-directional-beans) nests for the direction it has.

### Converting Map keys {#converting-map-keys}

A `Map` component's value leaf takes the component's name, so a key leaf carries `@MapKey` naming its component instead, as `noteKey()` does in `SupportDeskMapping`. Keys and values convert independently. Without a key leaf the key types must match exactly, and the processor refuses a mismatch, offering the annotation.

A key leaf may carry the component's [`@MapField`](basics.md#renamed-and-converted) rename only when it is named after the component it keys, since a rename renames the component its method is named after.

A failing key locates by the **source** key, the one the client sent, and an entry whose key and value both fail reports both there. A leaf over the whole `Map` wins over both: [a key leaf beside a whole-map leaf](rules.md#key-leaf-beside-a-whole-map-leaf) says when that is refused.

~~~admonish warning title="Not checked for you: a set silently drops a collapsed element"
A leaf that parses two wire values to one domain value (`"1"` and `"01"`, say) breaks the [section law](../optics/validated_prism.md#laws): an accepted wire value must rebuild to exactly itself. [`ValidatedPrismLaws`](../tooling/test_assertions.md#optic-laws) catches such a leaf, and [`ValidatedPrism.canonical`](../optics/validated_prism.md) rules it out. Where one slips through, a `Set` drops the duplicate silently. Two collapsed `Map` keys would lose a whole entry, so they are a located failure at the second key as it was sent, such as `notes.01: duplicates an earlier key`.
~~~

---

## Flattening a nested component onto a flat wire {#flattening-a-nested-component-onto-a-flat-wire}

Nesting assumes the wire nests too. When a courier's API, fixed by someone else, lists a collection point's [`Address`](basics.md#your-first-mapping) as plain `street`, `city` and `postcode` fields, no single wire component holds it, so no leaf can map it. `@Flatten` on a marker named after the component spreads it instead:

``` java
// A courier's collection point, whose API lists its address as plain fields.
record CollectionPoint(String name, Address address) {} // Address as on Record Mapping Basics

record CollectionPointDto(String name, String street, String city, String postcode) {} // flat

@GenerateMapping
interface CollectionPointMapping extends MappingSpec<CollectionPoint, CollectionPointDto> {
  @Flatten
  Address address(); // spread by name: street, city and postcode
}


    CollectionPointMappingImpl collectionMapping = CollectionPointMappingImpl.INSTANCE;

    CollectionPointDto flat =
        collectionMapping.build(
            new CollectionPoint("Kirkstall", new Address("1 High Street", "Leeds", "LS1 4AP")));
    // CollectionPointDto[name=Kirkstall, street=1 High Street, city=Leeds, postcode=LS1 4AP]
    Validated<NonEmptyList<FieldError>, CollectionPoint> missing =
        collectionMapping.parse(new CollectionPointDto("Kirkstall", null, "Leeds", null));
    // Invalid(NonEmptyList[address.street: must not be null, address.postcode: must not be null])
```

```mermaid
flowchart LR
    accTitle: A flattened address
    accDescr: Each member of the domain's Address record, address.street, address.city and address.postcode, pairs by name with the flat CollectionPointDto field of the same name.
    subgraph D["CollectionPoint.address"]
        AS["address.street"]
        AC["address.city"]
        AP["address.postcode"]
    end
    subgraph W["CollectionPointDto"]
        S["street"]
        C["city"]
        P["postcode"]
    end
    AS <-->|same name| S
    AC <-->|same name| C
    AP <-->|same name| P

    classDef domain fill:#a6d189,stroke:#40a02b,color:#232634
    classDef wire fill:#8caaee,stroke:#1e66f5,color:#232634
    class AS,AC,AP domain
    class S,C,P wire
```

In words: each member of the address pairs with the flat field of the same name, so a bad `street` comes back as `address.street`.

The record's components, spread this way, are the **group**. `parse` builds the `Address` from those fields, with their failures accumulating beside the collection point's, at the domain path `address.street`. The flat wire never sent that name, but like a [rename](basics.md#renames-mapfield), a path always uses domain names.

The vocabulary applies inside the group by name:

- a `@MapField` rename points a member at a differently named wire field
- a leaf converts a member, exactly as at the top level
- an [`@OptionalBridge`](absence.md#optional-bridge) bridges an `Optional` member
- a member that is itself a record nests through its own spec, and containers lift

A group whose members all copy as they are keeps [`asIso()`](tiers.md), and a mapping carrying a group nests like any other. Flattening onto a bean wire, a generic spec, a projection or a sparse `UpdateSpec` is not supported yet: [Where a flattened component can appear](rules.md#where-flattening-applies) and [Names in a flattened group](rules.md#names-in-a-flattened-group) have the rules.

---

## Across modules {#across-modules}

The spec a component nests through may live in another module. Keep the customer pair and `CustomerMapping` in `:orders-api` and the invoice pair in `:billing`, and `InvoiceMapping` still stays empty: its Impl delegates to the dependency's as it would to a sibling.

<!-- verify -->
```java
// :billing, which depends on :orders-api (Customer, CustomerDto and CustomerMapping live there)
@GenerateMapping
interface InvoiceMapping extends MappingSpec<Invoice, InvoiceDto> {}

// generated InvoiceMappingImpl.parse, the customer leg:
//   .field("customer", hkj$ifPresent(wire.customer(), CustomerMappingImpl.INSTANCE.asValidatedPrism()::parse))
```

There is nothing to configure, as long as `:orders-api` is compiled with `hkj-processor` on its processor path. A spec newly added to a dependency may need a clean downstream build before it is seen ([Multi-module builds](../tooling/manual_setup.md#multi-module-builds)).

When several specs could map a pair, the processor picks one this way:

```mermaid
flowchart TD
    accTitle: Which spec maps a pair
    accDescr: One spec for the pair in this module wins. With none, a single dependency's spec is used. With none anywhere, the pair needs a leaf or the build stops. Of two or more at one level, the most specific is used; where none is more specific they are ambiguous until a leaf, or at the dependency level your own spec, picks one.
    Q{"specs for the pair<br/>in this module?"} -->|one| O["yours wins"]
    Q -->|two or more| A1["the most specific,<br/>else a leaf picks one"]
    Q -->|none| D{"specs in<br/>dependencies?"}
    D -->|one| U["the dependency's"]
    D -->|two or more| A2["the most specific, with a note,<br/>else a leaf or your own spec"]
    D -->|none| N["no usable source,<br/>unless a leaf converts it"]

    classDef decision fill:#e5c890,stroke:#df8e1d,color:#232634
    classDef tier fill:#a6d189,stroke:#40a02b,color:#232634
    classDef error fill:#e78284,stroke:#d20f39,color:#232634
    class Q,D decision
    class O,U,A1,A2 tier
    class N error
```

In words: your own spec for the pair wins, and with none, a single dependency's is used. Of two or more at one level, the processor uses the most specific, as [The most specific spec for a pair is used](rules.md#generic-spec-precedence) says: a spec for exactly the pair before a generic one. Where none is more specific, the build fails with [`matches more than one mapping spec`](compiler_errors.md#more-than-one-spec) until you choose. A compiler note names a dependency's spec that another displaced, so a new dependency never quietly takes over a pair. [How a dependency's specs are found](rules.md#how-a-dependencys-specs-are-found) has the rest. A module with a `module-info.java` cannot resolve a dependency's specs yet.

~~~admonish tip title="Why this matters"
The delegation is an ordinary static reference in generated code, resolved at compile time from the dependency's class files: no runtime registry, no reflection, no service file to keep in step. Rename or remove a spec upstream and the downstream build fails at the use site, with the pair named, rather than a request failing later.
~~~

---

~~~admonish info title="Key Takeaways"
* **Nesting is delegation**: a spec for an inner pair is used wherever that pair appears, in this module or a dependency, and a `List` lifts it element by element
* **Error paths grow outward**: each level puts its name in front, so the client reads `customer.email` or `lines.1.price`, and a sealed dispatch adds no segment
* **Sealed dispatch is exhaustive both ways**: the processor refuses a missing subtype pair, so it never surprises you at runtime
* **A mapped `List`, `Set` or `Map` is unmodifiable**: one that needs no conversion is also a copy, never the other side's own list
~~~

~~~admonish tip title="See Also"
- [The null rule](basics.md#null-doctrine): Nulls inside containers are located too
- [The 422 leg](../spring/spring_boot_integration.md#the-422-leg): How these paths reach the client as one HTTP response
- [What Your Spec Generates](tiers.md): The methods a composed mapping offers
- [Generic Specs](generics.md): Nesting for generic records
- [Multi-module builds](../tooling/manual_setup.md#multi-module-builds): What the build needs when specs span modules
~~~

---

**Previous:** [Absent Fields and Record Invariants](absence.md)
**Next:** [Capstone: One 422, Every Bad Field](capstone.md)
