Glossary: Optics, Validation & Mapping

What This Page Covers

  • Lenses, prisms, traversals, the Focus DSL, and record mapping.
  • Part of the Glossary; see it for the other categories.

Affine

Definition: An optic that focuses on zero or one values within a structure. It combines the "might not be there" aspect of Prism with the "focus on part of a product" aspect of Lens, and composing a Lens with a Prism gives one.

Core Operations:

  • getOptional(S source) - Try to extract the value (returns Optional)
  • set(A value, S source) - Write the value; on an absent focus, an affine that can build the value writes it, and one that cannot leaves the structure unchanged
  • modify(Function<A, A> f, S source) - Transform the value only if it is present

Example:

// Affine for the first element of a list (might be empty)
Affine<List<String>, String> firstElement = Affine.of(
    list -> list.isEmpty() ? Optional.empty() : Optional.of(list.getFirst()),
    (list, newFirst) -> list.isEmpty() ? list :
        Stream.concat(Stream.of(newFirst), list.stream().skip(1)).toList()
);

List<String> items = List.of("a", "b", "c");
Optional<String> first = firstElement.getOptional(items);  // Optional[a]
List<String> updated = firstElement.set("X", items);   // ["X", "b", "c"]

List<String> empty = List.of();
Optional<String> noFirst = firstElement.getOptional(empty);  // Optional.empty
List<String> stillEmpty = firstElement.set("X", empty);  // [] (unchanged)

When To Use:

  • Accessing elements that may not exist (first element, element at index)
  • Optional fields in product types
  • Composing Lens with Prism (result is Affine)

Composition: Lens.andThen(Prism), Prism.andThen(Lens) and Affine.andThen(Lens) give an Affine; an Affine followed by a Traversal gives a Traversal.

Related: Lens, Prism, Affine Documentation


andThen

Definition: The method that joins two optics end to end, named and read like Function.andThen: the first optic runs first, and the second focuses inside what it reaches. UserLenses.address().andThen(AddressLenses.city()) is a Lens<User, String>. Every pair of Iso, Lens, Prism, Affine and Traversal has an overload. The result reaches as many values as the two steps allow together: a lens then a prism is an Affine, and any of the five then a traversal is a Traversal. On a Focus path the same join is spelled .via(...).

Related: Composition Rules, What a Path Is Made Of, Affine


At

Definition: A type class for structures that support indexed access with insertion and deletion semantics. Provides a Lens<S, Optional<A>> where setting to Optional.empty() deletes the entry and setting to Optional.of(value) inserts or updates it.

Core Operations:

  • at(I index) - Returns Lens<S, Optional<A>> for the index
  • get(I index, S source) - Read value at index (returns Optional)
  • insertOrUpdate(I index, A value, S source) - Insert or update entry
  • remove(I index, S source) - Delete entry at index
  • modify(I index, Function<A,A> f, S source) - Update value if present

Example:

At<Map<String, Integer>, String, Integer> mapAt = AtInstances.mapAt();

Map<String, Integer> scores = new HashMap<>(Map.of("alice", 100));

// Insert new entry
Map<String, Integer> withBob = mapAt.insertOrUpdate("bob", 85, scores);
// Result: {alice=100, bob=85}

// Remove entry
Map<String, Integer> noAlice = mapAt.remove("alice", withBob);
// Result: {bob=85}

// Compose with Lens for deep access
Lens<UserProfile, Optional<Integer>> themeLens =
    settingsLens.andThen(SettingsLenses.preferences()).andThen(mapAt.at("theme"));

When To Use: CRUD operations on maps or lists where you need to insert new entries or delete existing ones whilst maintaining immutability and optics composability.

Related: Indexed Access: At and Ixed Type Classes


Edits

Definition: A sparse, accumulating multi-edit over optics. Edits.combine(...) folds several pure edits (set, modify) into one reusable Update at compile time; a fallible edit is rejected there, so a validation failure can never be silently dropped. Edits.accumulate(...) adds the validated REST-PATCH shape: it mixes pure and fallible edits, reports every bad field at once (each located as a FieldError), and applies the writes only if all validated. The …IfPresent factories treat null as absent, which is what makes a patch sparse.

Example:

import static org.higherkindedj.optics.edit.Edit.*;

// Pure fold: several edits into one reusable Update<Order>
Update<Order> normalise = Edits.combine(
    modify(EMAIL, String::toLowerCase),
    modify(SKU,   String::trim));

// Validated PATCH: every bad field reported at once, only present fields written
Validated<NonEmptyList<FieldError>, Order> patched =
    Edits.accumulate(
            setIfPresent(ORDER_NUMBER, req.orderNumber()),
            parseIfPresent(CONTACT, req.email(), Email::parse),
            modifyIfPresent(QUANTITY, req.qtyDelta(), (delta, qty) -> qty + delta))
        .apply(order);

Related: Many Edits at Once, Update, FieldError, ValidatedPrism


ErrorEnvelope

Definition: The record that carries an error's envelope as one value: ErrorEnvelope<C>(String code, String message, Instant timestamp, C context), where C is a typed context record rather than a Map<String, Object>. A sealed error hierarchy declares one ErrorEnvelope<C> component per variant, and @GenerateErrorEnvelope generates the factories and builders around it. Two verbs keep enrichment honest: withContext(D) is the record wither that replaces the context (and may change its type), while the generated editContext transforms the existing one.

Example:

record OutOfStock(List<ProductId> products, ErrorEnvelope<OrderErrorContext> envelope)
    implements OrderError {}

ErrorEnvelope<OrderErrorContext> envelope =
    ErrorEnvelope.of(timeSource, "OUT_OF_STOCK", "Out of stock", context);

Related: @GenerateErrorEnvelope, Merge and Error Envelopes, TimeSource


FieldError

Definition: A single validation failure carrying a composable path to the offending field plus a message. It is a small record (path segments plus a message) with a pathString() such as "address.zip". Accumulating validation collects FieldErrors into a NonEmptyList in declaration order, so a failed parse reports which fields were wrong and where they sit in a nested structure, not merely that validation failed.

Example:

FieldError bare    = FieldError.of("not a postcode");   // unlocated leaf
FieldError located = bare.at("zip").at("address");      // pathString() == "address.zip"

Located automatically: Validated.fields() and the parseIfPresent edits prepend the field label onto each error's path, so a leaf validator creates unlocated FieldError.of(...)s and the assembly attaches the location. hkj-test asserts the whole accumulation with assertThatValidated(result).hasFieldErrors(...), and a single error's path with assertThatFieldError.

Related: Open-Arity Assembly, NonEmptyList, Validated Assembly, ValidatedPrism


Focus DSL

Definition: A domain-specific language for fluent, type-safe navigation and manipulation of immutable data structures. The Focus DSL provides a composable way to build paths through nested records without manual lens composition.

Core Concept: Instead of composing optics manually, the Focus DSL lets you chain .via(...) hops off a generated path, with the optic types inferred automatically. Through a navigator, a hop can be named after its field instead, such as .address().

Example:

// Without Focus DSL: manual lens composition
Lens<Employee, String> streetLens =
    EmployeeLenses.company()
        .andThen(CompanyLenses.address())
        .andThen(AddressLenses.street());
String composedStreet = streetLens.get(employee);

// With Focus DSL: the generated paths compose, and the source is named once, at the end
String street = EmployeeFocus.company()
    .via(CompanyFocus.address())
    .via(AddressFocus.street())
    .get(employee);

// Modification is equally fluent
Employee updated = EmployeeFocus.company()
    .via(CompanyFocus.address())
    .via(AddressFocus.city())
    .modify(String::toUpperCase, employee);

// Mix with Effect Paths for effectful navigation
EitherPath<AppError, String> effectfulCity = employeeService.findById(id)
    .focus(EmployeeFocus.company())
    .focus(CompanyFocus.address())
    .focus(AddressFocus.city());

Key Features:

  • Type-safe: Compiler catches invalid paths
  • Composable: Chain any optic types together
  • Generated: @GenerateFocus creates Focus helpers automatically
  • Effect integration: Seamlessly works with Effect Paths

Related: FocusPath, Lens, Effect-Optics Bridge, Focus DSL Documentation


Focus path type

Definition: The type of a Focus path, fixed by how many values it reaches:

Path typeReachesWraps
FocusPathexactly one valueLens
AffinePathzero or one valueAffine
TraversalPathzero or more valuesTraversal

toLens(), toAffine() and toTraversal() hand over the optic inside, and Path widening decides which type a generated method returns.

Related: FocusPath, What a Path Is Made Of


FocusPath

Definition: The Focus DSL's path type for a field that is always there: a FocusPath<S, A> reaches exactly one value and wraps a Lens<S, A>. @GenerateFocus on a record generates a FooFocus class whose static methods return these paths, or an AffinePath for an optional field and a TraversalPath for a collection.

Generation: Add @GenerateFocus to your record to generate the corresponding Focus class.

Example:

@GenerateFocus
public record User(String name, Address address, List<Order> orders) {}

@GenerateFocus
public record Address(String street, String city, String postcode) {}

// Generated: UserFocus class with methods:
// - UserFocus.name()     → FocusPath<User, String>
// - UserFocus.address()  → FocusPath<User, Address>
// - UserFocus.orders()   → TraversalPath<User, Order>

// Use in Focus DSL
String city = UserFocus.address()
    .via(AddressFocus.city())
    .get(user);

// Compose for reusable paths
FocusPath<User, String> userCity = UserFocus.address()
    .via(AddressFocus.city());

// Use with Effect Paths
EitherPath<AppError, String> cityPath = loadUser(id)
    .focus(UserFocus.address())
    .focus(AddressFocus.city());

Naming Convention:

  • Record Foo generates FooFocus class
  • Each field bar generates static method FooFocus.bar()

Related: Focus DSL, Lens, Code Generation


Fold

Definition: A read-only optic that extracts zero or more values from a structure. Folds are like Traversals but without the ability to modify. They generalise the concept of "folding" or "reducing" over a structure.

Core Operations:

  • foldMap(Monoid<M> monoid, Function<A, M> f, S source) - Map and combine all values
  • toList(S source) - Extract all focused values as a list
  • headOption(S source) - Get the first value if any
  • exists(Predicate<A> p, S source) - Check if any value satisfies predicate
  • all(Predicate<A> p, S source) - Check if all values satisfy predicate

Example:

// Fold over all players in a league
Fold<League, Player> allPlayers = LeagueFolds.teams()
    .andThen(TeamFolds.players());

// Extract all players
List<Player> players = allPlayers.getAll(league);

// Sum all scores using a Monoid
Integer totalScore = allPlayers.foldMap(
    Monoids.integerAddition(),
    Player::score,
    league
);

// Check conditions across all values
boolean anyInactive = allPlayers.exists(p -> !p.isActive(), league);
boolean allQualified = allPlayers.all(p -> p.score() >= 100, league);

When To Use:

  • Extracting multiple values without modification
  • Aggregating data from nested structures
  • Querying collections within complex types
  • When you need read-only access to multiple elements

Related: Traversal, Getter (Fold of one)


@GenerateAssembly

Definition: The codegen companion for Validated Assembly. Annotate a record and the processor emits a same-package …Assembly companion with one order-enforcing method per component, so assembly is discovered by autocomplete and the canonical constructor is baked in. A component typed as another annotated record accepts its sub-companion's result directly.

Example:

@GenerateAssembly
public record User(Name name, Email email) {}

Validated<NonEmptyList<FieldError>, User> user =
    UserAssembly.fields()
        .name(parseName(dto.name()))
        .email(parseEmail(dto.email()))
        .assemble();      // canonical constructor baked in

Related: Open-Arity Assembly, Validated Assembly, FieldError


@GenerateErrorEnvelope

Definition: The third record-mapping processor, targeting the typed domain error a fallible mapping produces. A sealed error hierarchy usually re-declares the same envelope (code, message, timestamp, context) on every variant, with context an untyped Map<String, Object>. @GenerateErrorEnvelope supplies the envelope and types the context: each variant declares only its domain fields plus one ErrorEnvelope<C> component, and the processor generates the …s companion (per-variant factories, a typed context() builder, and an editContext wither). Context is records-as-schema (context.orderId(), not map.get(...)); timestamps read from a TimeSource for deterministic tests.

Example:

record OrderErrorContext(@Nullable OrderId orderId, @Nullable TraceId traceId) {}

@GenerateErrorEnvelope
public sealed interface OrderError {
  ErrorEnvelope<OrderErrorContext> envelope();                  // declared once

  default OrderError editContext(UnaryOperator<OrderErrors.ContextBuilder> edit) {
    return OrderErrors.editContext(this, edit);                 // the companion does the work
  }

  record OutOfStock(List<ProductId> products,
                    ErrorEnvelope<OrderErrorContext> envelope) implements OrderError {}
}

OrderError error = OrderErrors.outOfStock(products)
    .editContext(ctx -> ctx.orderId(orderId).traceId(traceId));  // typed context, not map.put

Related: Record Mapping, TimeSource, @GenerateMapping


@GenerateMapping

Definition: An annotation processor for the record-to-DTO boundary. Annotate an interface extending MappingSpec<Domain, Wire> and the processor generates, reflection-free at compile time, a total build (domain to wire) plus an accumulating parse (wire to domain) returning Validated<NonEmptyList<FieldError>, Domain>, so a bad DTO reports every bad field at once. Components match by name and type; @MapField declares renames, @Flatten spreads a nested record across a flat wire, and List, Set, array, Optional and Map containers lift automatically - a map's keys too, with @MapKey. The annotation sits on your spec interface, so third-party records map without being annotatable.

Example:

@GenerateMapping
public interface PersonMapping extends MappingSpec<Person, PersonDto> {}

PersonMappingImpl personMapping = PersonMappingImpl.INSTANCE;     // bind once, reuse
PersonDto dto = personMapping.build(person);                      // total
Validated<NonEmptyList<FieldError>, Person> back =
    personMapping.parse(dto);                                     // accumulating, located

Truthful emission tiers: the generated mapper offers only what the shape supports: asIso when lossless, asLens when total one way, and the accumulating parse otherwise. A spec extending UpdateSpec instead opts into the sparse PATCH tier (only updateFrom). Every tier is law-checked against the published hkj-test harness.

Related: Record Mapping, ValidatedPrism, FieldError, @GenerateMerge, UpdateSpec


@GenerateMerge

Definition: The forward-only sibling of @GenerateMapping: assemble one target record from several sources, declared entirely by the spec method's signature, with no class literals and no inverse. Each target component fills from the one source with a same-named component: identity when the types match, through a ValidatedPrism leaf when they differ, or through a sibling @GenerateMapping spec (failures locating as dotted paths). Ambiguous or unfilled components are what/why/fix compile errors, and the return type must tell the truth: a fallible fill demands a Validated return.

Example:

@GenerateMerge
public interface DashboardAssembly {
  Dashboard assemble(User user, Account account, Settings settings);
}

Dashboard dashboard =
    DashboardAssemblyImpl.INSTANCE.assemble(user, account, settings);

Related: Record Mapping, @GenerateMapping, ValidatedPrism


Iso (Isomorphism)

Definition: An optic representing a lossless, bidirectional conversion between two types. If you can convert A to B and back to A without losing information, you have an isomorphism.

Core Operations:

  • get(S source) - Convert from S to A
  • reverseGet(A value) - Convert from A to S

Example:

// String and List<Character> are isomorphic
Iso<String, List<Character>> stringToChars = Iso.of(
    s -> s.chars().mapToObj(c -> (char) c).collect(Collectors.toList()),
    chars -> chars.stream().map(String::valueOf).collect(Collectors.joining())
);

List<Character> chars = stringToChars.get("Hello");  // ['H', 'e', 'l', 'l', 'o']
String back = stringToChars.reverseGet(chars);       // "Hello"

When To Use: Converting between equivalent representations (e.g., Celsius/Fahrenheit, String/ByteArray, domain models and DTOs with no information loss).

Related: Iso Documentation


Lens

Definition: An optic for working with product types (records with fields). Provides a composable way to get and set fields in immutable data structures.

Core Operations:

  • get(S source) - Extract a field value
  • set(A newValue, S source) - Create a new copy with updated field
  • modify(Function<A,A> f, S source) - Update field using a function

Example:

@GenerateLenses
public record Address(String street, String city) {}

@GenerateLenses
public record Company(String name, Address address) {}

@GenerateLenses
public record Employee(String name, Company company) {}

// Compose lenses for deep updates
Lens<Employee, String> employeeToStreet =
    EmployeeLenses.company()
        .andThen(CompanyLenses.address())
        .andThen(AddressLenses.street());

// Update nested field in one line
Employee updated = employeeToStreet.set("456 New St", originalEmployee);

Related: Lenses Documentation


MappingLaws

Definition: The hkj-test law harness for generated mappings. One assertMappingLaws call per @GenerateMapping Impl verifies the laws of its emission tier: the iso laws plus parse-iso coherence for a lossless mapping, the lens laws for a projection, the round-trip and no-parse laws for a fallible mapping, the patch laws (projection identity, idempotence, located validation) for a validated patch, and the identity/idempotence/validation laws for a sparse UpdateSpec updateFrom. The same harness law-checks every golden Impl in the library's own build.

Example:

import org.higherkindedj.optics.laws.MappingLaws;

MappingLaws.assertMappingLaws(
    PersonMappingImpl.INSTANCE.asValidatedPrism(), validDto, invalidDto);

Related: Record Mapping, Testing With hkj-test, @GenerateMapping


MappingSpec

Definition: The marker interface a mapping spec extends to name its pair: interface UserMapping extends MappingSpec<Domain, Wire> {}. The interface deliberately declares nothing callable (the generated UserMappingImpl carries the surface); the spec's members are the declaration vocabulary: default ValidatedPrism methods are leaves, @MapField methods are renames (an abstract marker, or the component's leaf when it also converts), default Getter methods are derived wire fields, @OptionalBridge marks a component whose null means absent, @MapKey names the Map component whose keys a leaf converts, @Flatten marks a nested record component spread across the wire's flat components. Its sparse sibling is UpdateSpec, which swaps the whole generated surface for a single updateFrom.

Example:

@GenerateMapping
public interface UserMapping extends MappingSpec<User, UserDto> {}

Related: @GenerateMapping, Record Mapping Basics, UpdateSpec


modifyF

Definition: The general form of modify, on every optic that writes and on every Focus path. The function returns its new value inside an effect, such as a CompletableFuture or a Validated, and modifyF returns the whole updated structure inside that effect. It takes the effect's Applicative, the object that combines results inside that effect (a Functor is enough for a Lens or a FocusPath), and works in Kind, the library's encoding of a generic container. For a lens or a traversal, OpticOps.modifyEither, modifyMaybe, modifyAllEither and modifyAllValidated are the shorter call.

Related: Updates That Can Fail, Type Class and Effect Integration, Traversal


Definition: A path the Focus processor generates for a field whose type is another @GenerateFocus record, given a type for each of its type parameters if it has any. An Either of such a record gets one too, and so does a Map or an array the record widens. A navigator is the field's own path, a FocusPath, AffinePath or TraversalPath, with one method added per field of that record. So UserFocus.address().city() chains where a plain path needs .via(AddressFocus.city()), and the navigator still goes wherever its path goes:

The field on UserIts method returns
Address addressAddressNavigator<User>, a FocusPath<User, Address>
@Nullable Address billingBillingNavigator<User>, an AffinePath<User, Address>
Map<String, Address> sites, named in widenFieldsSitesNavigator<User>, a TraversalPath<User, Address>

Navigators are on by default; @GenerateFocus(generateNavigators = false) turns them off.

Related: Collections, Optionals and Sealed Types, Focus DSL, Focus path type


Parse, Don't Validate

Definition: The principle that a boundary should turn unstructured input into a typed value once, at the edge, and keep that guarantee in the type thereafter, rather than re-checking the same data repeatedly downstream. Higher-Kinded-J expresses it with types whose parse is fallible and accumulating and whose build is total: ValidatedPrism for a single value, Validated Assembly for a whole record, and @GenerateMapping for a record-to-DTO boundary. Failures are FieldErrors, so a rejected input reports every bad field at once, each located.

Related: Record Mapping, ValidatedPrism, Validated


Path widening

Definition: How a path's type follows the shape of the field it reaches. A field that may hold nothing, such as an Optional or a component with a recognised @Nullable, widens a path to an AffinePath. One that may hold many, such as a List, widens it to a TraversalPath. Composing keeps the wider of the two types. The processor settles it at compile time from the declared type, so a nullable field nobody annotated with a recognised @Nullable stays a FocusPath, and you chain .nullable() for it. A Map or an array of a reference type stays a FocusPath too, unless the annotation sets widenCollections = true.

Related: Focus path type, Collections, Optionals and Sealed Types


Prism

Definition: An optic for working with sum types (sealed interfaces, Optional, Either). Provides safe access to specific variants within a discriminated union.

Core Operations:

  • preview(S source) - Try to extract a variant (returns Optional)
  • review(A value) - Construct the sum type from a variant
  • modify(Function<A,A> f, S source) - Update if variant matches

Example:

@GeneratePrisms
public sealed interface Payment permits Card, Bank {}
public record Card(String pan) implements Payment {}
public record Bank(String iban) implements Payment {}

Prism<Payment, Card> cardPrism = PaymentPrisms.card();

// Safe extraction
Optional<String> pan = cardPrism.preview(payment).map(Card::pan);

// Conditional update: only a Card is touched
Payment masked = cardPrism.modify(card -> new Card("****" + card.pan()), payment);

Related: Prisms Documentation, ValidatedPrism


Setter

Definition: A write-only optic that can modify zero or more values within a structure. Setters are the dual of Folds: where Folds can only read, Setters can only write. They cannot extract values, only transform them.

Core Operations:

  • modify(Function<A, A> f, S source) - Apply function to all focused values
  • set(A value, S source) - Set all focused values to same value

Example:

// A setter for an order's line items, and one for an item's price
Setter<Order, List<LineItem>> allItems = OrderSetters.items();
Setter<LineItem, BigDecimal> itemPrice = LineItemSetters.price();

// Apply a discount to every price: the outer setter rewrites the list, the inner each element
Order discounted = allItems.modify(
    items -> items.stream()
        .map(item -> itemPrice.modify(price -> price.multiply(new BigDecimal("0.9")), item))
        .toList(),
    order
);

// Set one price to zero (for testing)
LineItem zeroed = itemPrice.set(BigDecimal.ZERO, lineItem);

// Compose with other setters, one component at a time
Setter<Employee, String> employeeCompanyName =
    EmployeeSetters.company().andThen(CompanySetters.name());

Employee normalised = employeeCompanyName.modify(String::toLowerCase, employee);

When To Use:

  • Bulk modifications without needing to read values
  • Applying transformations across nested structures
  • When modification logic doesn't depend on current values
  • Composing write-only operations

Related: Traversal, Fold


StandardCodecs

Definition: The stock ValidatedPrism vocabulary for the common wire-to-domain conversion families: one static factory per family (uuid(), uri(), localDate(), instant(), offsetDateTime(), enumByName(Class), bigDecimal(), intFromString(), booleanStrict(), currency(), locale(), and friends). Each codec is lawful by construction (built on ValidatedPrism.canonical, accepting exactly the canonical form it renders). Every failure is a FieldError whose message names the spelling expected. A codec's own failure carries no path: the generated parse locates it at the component the codec converts. Codecs are ordinary leaves: a spec declares them as default methods, and nothing is ever applied implicitly.

Example:

public record Shipment(UUID id, LocalDate placed) {}

public record ShipmentDto(String id, String placed) {}

@GenerateMapping
public interface ShipmentMapping extends MappingSpec<Shipment, ShipmentDto> {
  default ValidatedPrism<String, UUID> id() { return StandardCodecs.uuid(); }
  default ValidatedPrism<String, LocalDate> placed() { return StandardCodecs.localDate(); }
}

Related: Standard codecs, ValidatedPrism, @GenerateMapping


Traversal

Definition: An optic for working with multiple values within a structure (lists, sets, trees). Allows bulk operations on all elements.

Core Operations:

  • modifyF(Applicative<F> app, Function<A, Kind<F,A>> f, S source) - Effectful modification of all elements
  • toList(S source) - Extract all focused values as a list

Example:

@GenerateTraversals
public record Basket(String id, List<LineItem> items) {}

Traversal<Basket, LineItem> basketItems = BasketTraversals.items();

// Apply bulk update
Basket discounted = Traversals.modify(
    basketItems,
    item -> new LineItem(item.sku(), item.price().multiply(new BigDecimal("0.9"))),
    basket
);

Related: Traversals Documentation


UpdateSpec

Definition: The sparse-PATCH sibling of MappingSpec. Annotate an interface extending UpdateSpec<Domain, Wire> (with @GenerateMapping) to opt into the null-as-absent contract: a null bean property means not provided, leave unchanged rather than broken data. The processor generates a single method, updateFrom(Wire) : Edits.Accumulated<Domain> (no build, parse, or as* tier), folding the present (non-null) properties into an Update and skipping the absent ones. One spec names one tier, so an interface extending UpdateSpec must not also extend MappingSpec. Over a protobuf-java message it generates updateFrom(Wire, FieldMask) instead, which edits the fields the mask names, each parsed as parse would read it. Otherwise the wire must be bean-shaped, and a primitive property is rejected (it can never be absent); every property's getter must answer null (or undefined(), for a JsonNullable) until set, which is not checked, since a default the bean gives itself (a field initialiser, a constructor assignment, a getter that creates its value) reads as sent; a domain Optional without a whole-component leaf is patchable from an Optional-typed property whose field defaults to null (a present empty Optional encodes "set to empty"), or from a JsonNullable one (a sent null encodes it). A present container parses through the element leaf lifted over it (the same vocabulary the dense tiers lift, each failing element located: phones.1), with a whole-container leaf winning as the more specific declaration. Present-but-invalid fields still fail, located and accumulating.

Example:

// The wire is a bean, not a record: null means "not provided"
public class UserPatchDto {
  private String name;

  public String getName() { return name; }

  public void setName(String name) { this.name = name; }
}

@GenerateMapping
public interface UserPatchMapping extends UpdateSpec<PatchableUser, UserPatchDto> {}

Edits.Accumulated<PatchableUser> patch = UserPatchMappingImpl.INSTANCE.updateFrom(patchDto);
Validated<NonEmptyList<FieldError>, PatchableUser> updated =
    patch.apply(current);                                    // absent fields survive

Related: Record Mapping, @GenerateMapping, Edits


Validated Assembly

Definition: Open-arity assembly of a record from N independently validated fields, with every error collected and no Semigroup argument, no arity wall, and no Kind ceremony. Validated.fields() opens a labelled assembly over NonEmptyList<FieldError>; each field(label, value) adds one validated field, and apply(...) completes it with a constructor reference of exactly the accumulated arity, or construct(...) when that constructor may refuse the fields, reporting its exception as a FieldError. The same shape exists across three carriers: Validated (strict), ValidationPath (railway, via Path.fields()), and EitherOrBoth (tolerant).

Example:

record Profile(Name name, Email email) {}

Validated<NonEmptyList<FieldError>, Profile> profile =
    Validated.fields()
        .field("name",  parseName(dto.name()))
        .field("email", parseEmail(dto.email()))
        .apply(Profile::new);
// Invalid(NonEmptyList[email: not an email address]), or Valid(user)

Related: Open-Arity Assembly, @GenerateAssembly, FieldError, Validated, EitherOrBoth


ValidatedPrism

Definition: The smart-constructor optic for parse, don't validate boundaries. Its parse returns Validated<NonEmptyList<FieldError>, A>, so every failure is located rather than only the first, whilst build is total and always succeeds. It is the accumulating counterpart to a Prism, whose preview reports only presence or absence.

Example:

ValidatedPrism<String, EmailAddress> email = ValidatedPrism.of(
    raw -> parseEmailAddress(raw), // String -> Validated<NonEmptyList<FieldError>, EmailAddress>
    EmailAddress::toString);       // total build

Validated<NonEmptyList<FieldError>, EmailAddress> parsed = email.parse("  NOPE ");
String rendered = email.build(addr);   // always succeeds

Composition: nested composition short-circuits whilst sibling fields accumulate, so a whole record parses in one pass with every bad field reported. ValidatedPrism.canonical(message, parse, render) wraps a throwing parser with the section law guarded per value: the render defines the canonical form and every spelling it cannot reproduce is a located rejection (an injective render stays your obligation); ValidatedPrism.fromIso(iso) is a parse that never fails; ValidatedPrism.fromPrism(prism, reason) lifts a plain prism by supplying the reason its empty case cannot express. Both round-trip laws ship as ValidatedPrismLaws in hkj-test.

Halves: each direction is also a type of its own, ValidatedParse (parse and its bulk forms) and ValidatedBuild (build and its bulk forms). Every prism is both, so it serves wherever either is asked for, and a boundary crossed one way only, such as a one-directional bean mapping, exposes just the half it has.

Related: ValidatedPrism, Prism, FieldError, Validated


Previous: Effect Paths & Handlers Next: Concurrency & Resilience