Annotations at a Glance

Every annotation that generates an optic: what it goes on, and what it writes.

Every optic in this chapter is generated by an annotation. You write a record or sealed type, add the annotation, and the processor produces a typed companion class at compile time. There are no runtime reflection costs and no boilerplate to maintain.

This page is the lookup table. Each row links to the page that explores the annotation in depth.


1. Generate optics for your records

Apply these to your own records and sealed types. The generated class is placed in the same package by default; pass targetPackage = "..." to override. Every type it names has to be visible from that package, so keep a nested type it uses package-private rather than private (Compiler Messages).

AnnotationApply toGeneratesWhen to reach for it
@GenerateLensesrecordXLenses (one lens per field, plus withFoo helpers)Default starting point for any record you want to update functionally
@GenerateFocusrecordXFocus (path-based DSL builder)Pair with @GenerateLenses when you want fluent, IDE-friendly navigation
@GenerateFoldsrecordXFolds (read-only optics)Querying without modification (CQRS-friendly)
@GenerateGettersrecordXGetters (asymmetric read-only)When you specifically want a Getter rather than a full Lens
@GenerateSettersrecordXSetters (asymmetric write-only)When you specifically want a Setter rather than a full Lens
@GenerateTraversalsrecord containing List<T> etc.XTraversals (one traversal per traversable field)Bulk operations on a collection embedded in a record
@GeneratePrismssealed interface or enumXPrisms (one prism per variant)Sum types, including both sealed hierarchies and enum constants
@GenerateIsosstatic, no-argument method returning Iso<A, B>, naming no type variablecompanion class with the iso as a static fieldLossless conversions between equivalent representations

Annotations stack

You almost always want at least @GenerateLenses and @GenerateFocus together. Add @GenerateTraversals if the record contains a collection field and @GenerateFolds if you also need read-only queries. The chapter's Order stacks three, and its Focus path chains through its Customer by a navigator:

@GenerateLenses
@GenerateFocus
@GenerateTraversals
public record Order(
    UUID id,
    Customer customer,
    List<LineItem> lines,
    Instant placedAt,
    Currency currency,
    OrderStatus status) {}

2. Generate optics for types you don't own

External types like LocalDate, Jackson's JsonNode, JOOQ records, and Protobuf messages can't be annotated directly. Two gateways bring them into the optics world.

AnnotationApply toUse when
@ImportOpticspackage-info.java or a typeThe external type is a record (lenses), sealed interface (prisms), or enum (prisms) and the processor can analyse it directly
OpticsSpec<S>extend in your own interfaceThe external type defies auto-detection (no sealed hierarchy, no copy mechanism, predicate-based type checks). You declare what you want; hint annotations say how to build it.

OpticsSpec interfaces use the spec-method hints below to tell the processor how to generate each optic.


3. Generate record mappings (domain ↔ DTO)

AnnotationApply toGeneratesWhen to reach for it
@GenerateMappinginterface extending MappingSpec<Domain, Wire>XMappingImpl with a total build and an accumulating, located parseBidirectional boundary mapping with validation: every bad field reported at once
@GenerateMappinginterface extending UpdateSpec<Domain, Wire> (bean wire, or a protobuf-java message)XMappingImpl with only updateFrom(Wire) : Edits.Accumulated<Domain>, or updateFrom(Wire, FieldMask) over a messageSparse PATCH write-back: fold the present (non-null) request fields into an update, leave the absent ones; over a message, apply every field its FieldMask names, set or not, and leave the rest
@MapField(to = "...")abstract marker method on the spec, named after the domain component, or that component's default leafa rename in both directionsDomain and wire components with different names
@OptionalBridgeabstract marker method on the spec, or that component's default leafa domain Optional<T> mapped to a nullable wire component TA record wire that encodes absence as null, the way a JSON binder writes it
@MapKey("component")default leaf on the spec, freely named, returning ValidatedPrism<WireKey, DomainKey>the named Map component's keys converted, alone or beside its value leafTwo sides that key a map differently (Map<String, ...> on the wire, Map<Locale, ...> in the domain)
@Flattenabstract marker method on the spec, named after a nested domain record componentthat record's components spread across the wire's flat components by name, both directionsA wire fixed flat (street, city, postcode) where the domain nests an Address
@GenerateAssemblyrecordXAssembly, a staged builder over Validated<NonEmptyList<FieldError>, R> with one method per componentBuilding a record from independently validated parts, collecting every error, with no arity ceiling
@GenerateMergeinterface whose abstract method names target and sourcesa forward-only assembly of one target record from several sourcesMerging several records into one; no inverse is generated, because the multi-source case has none
@GenerateErrorEnvelopesealed interface whose variants each carry one ErrorEnvelope<C>per-variant factories, a fluent context builder, and a context witherGiving a sealed error hierarchy a typed context without repeating it on every variant

Leaves, nesting, List/Set/array/Optional/Map lifting, sealed dispatch, the asIso()/asLens() tiers, and the sparse UpdateSpec tier are covered in Record Mapping.


4. Spec-method hints (inside OpticsSpec interfaces)

Apply these to abstract methods inside an interface that extends OpticsSpec<S>.

Prism hints, for sum-type-like external types

AnnotationGenerates a prism via
@InstanceOf(SubType.class)Java instanceof pattern matching (e.g. Jackson's ObjectNode, ArrayNode)
@MatchWhenPredicate method (isFoo()) plus a getter (asFoo()), for type-checking APIs that don't use sealed types

A parameterised @InstanceOf target may only promise the type arguments the source type pins down: the class constant is raw, and the test runs after erasure. See Parameterised Targets.

Lens hints, copy strategies for legacy Java

External record-like types rarely have a single copy mechanism. The processor uses these hints to know how to rebuild the object after a set or modify.

AnnotationGenerates a lens that copies via
@WitherA withFoo(value) method on the source
@ViaBuilderThe builder pattern (source.toBuilder().foo(value).build())
@ViaConstructorThe all-args constructor
@ViaCopyAndSetA copy constructor followed by a setter call (legacy bean style)

Traversal hints

AnnotationGenerates
@ThroughFieldA traversal composing a lens-to-field with the auto-detected container traversal
@TraverseWithA traversal using an explicitly named Traverse instance

5. Kind-field configuration (on a record component)

This one is not a spec-method hint: it targets a record component, so it goes on the field of your own record, not on a method of an OpticsSpec interface.

AnnotationApply toEffect
@TraverseFieldrecord component of type Kind<F, A>Names the Traverse<F> instance the Focus DSL generator should walk the field with, and the cardinality that decides the path type it generates

KindSemantics is an enum, not a second annotation: it is the value of @TraverseField's own semantics element. traverse is required and takes a fully qualified expression yielding a Traverse<F>, such as a singleton field or a factory call:

@TraverseField(
    traverse = "com.example.TreeTraverse.INSTANCE",
    semantics = KindSemantics.ZERO_OR_ONE)

semantics defaults to ZERO_OR_MORE, which generates a TraversalPath; EXACTLY_ONE and ZERO_OR_ONE both generate an AffinePath.


6. Build setup

The HKJ Gradle and Maven plugins wire the annotation processor in for you. Apply the plugin and every annotation on this page is available immediately. See Build Plugins: One-Line HKJ Setup for the plugin DSL, or Manual Setup if you need to configure dependencies by hand.

For compile-time path-type checking, see Compile-Time Checks.


7. Quick decision guide

You have...You want to...Reach for
A recordGet/set fields functionally@GenerateLenses
A recordNavigate deeply with a fluent DSL@GenerateLenses + @GenerateFocus
A record with a List fieldUpdate every element@GenerateTraversals
A recordQuery without modifying@GenerateFolds
A sealed interface or enumOperate on one variant@GeneratePrisms
Two equivalent typesConvert losslessly@GenerateIsos on a method
An external record (in a library)Lenses for its components@ImportOptics
An external sealed/enum (in a library)Prisms for its variants@ImportOptics
JsonNode, JOOQ records, anything trickyCustom optics with copy strategy@ImportOptics on an OpticsSpec interface + spec-method hints
A domain record and a wire DTOMap both ways, with validation@GenerateMapping on a MappingSpec interface
A domain record and a PATCH request beanFold the present fields into an update, leave the absent ones@GenerateMapping on an UpdateSpec interface

See Also


Previous: Production Readiness Next: Decision Trees