Annotations at a Glance
The complete optic-generation surface in one page
- Which annotation to apply for each optic type, and what target each annotation expects (record, sealed interface, enum, method, package-info).
- How to choose between
@ImportOpticsand anOpticsSpecinterface for types you don't own. - The eight spec-method hints that drive optic generation for external types: two prism matchers, four lens copy strategies, and two traversal hints.
- Where in the book to look for the in-depth treatment of each annotation.
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.
| Annotation | Apply to | Generates | When to reach for it |
|---|---|---|---|
@GenerateLenses | record | XLenses (one lens per field, plus withFoo helpers) | Default starting point for any record you want to update functionally |
@GenerateFocus | record | XFocus (path-based DSL builder) | Pair with @GenerateLenses when you want fluent, IDE-friendly navigation |
@GenerateFolds | record | XFolds (read-only optics) | Querying without modification (CQRS-friendly) |
@GenerateGetters | record | XGetters (asymmetric read-only) | When you specifically want a Getter rather than a full Lens |
@GenerateSetters | record | XSetters (asymmetric write-only) | When you specifically want a Setter rather than a full Lens |
@GenerateTraversals | record containing List<T> etc. | XTraversals (one traversal per traversable field) | Bulk operations on a collection embedded in a record |
@GeneratePrisms | sealed interface or enum | XPrisms (one prism per variant) | Sum types, including both sealed hierarchies and enum constants |
@GenerateIsos | static, no-argument method returning Iso<A, B>, naming no type variable | companion class with the iso as a static field | Lossless conversions between equivalent representations |
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.
@GenerateLenses
@GenerateFocus
@GenerateTraversals
public record Order(Customer customer, List<LineItem> items) {}
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.
| Annotation | Apply to | Use when |
|---|---|---|
@ImportOptics | package-info.java or a type | The 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 interface | The 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)
| Annotation | Apply to | Generates | When to reach for it |
|---|---|---|---|
@GenerateMapping | interface extending MappingSpec<Domain, Wire> | XMappingImpl with a total build and an accumulating, located parse | Bidirectional boundary mapping with validation: every bad field reported at once |
@GenerateMapping | interface extending UpdateSpec<Domain, Wire> (bean wire) | XMappingImpl with only updateFrom(Wire) : Edits.Accumulated<Domain> | Sparse PATCH write-back: fold the present (non-null) request fields into an update, leave the absent ones |
@MapField(to = "...") | abstract method on the spec, named after the domain component | a rename in both directions | Domain and wire components with different names |
@GenerateAssembly | record | XAssembly, a staged builder over Validated<NonEmptyList<FieldError>, R> with one method per component | Building a record from independently validated parts, collecting every error, with no arity ceiling |
@GenerateMerge | interface whose abstract method names target and sources | a forward-only assembly of one target record from several sources | Merging several records into one; no inverse is generated, because the multi-source case has none |
@GenerateErrorEnvelope | sealed interface whose variants each carry one ErrorEnvelope<C> | per-variant factories, a fluent context builder, and a context wither | Giving a sealed error hierarchy a typed context without repeating it on every variant |
Leaves, nesting, List/Optional 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
| Annotation | Generates a prism via |
|---|---|
@InstanceOf(SubType.class) | Java instanceof pattern matching (e.g. Jackson's ObjectNode, ArrayNode) |
@MatchWhen | Predicate 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.
| Annotation | Generates a lens that copies via |
|---|---|
@Wither | A withFoo(value) method on the source |
@ViaBuilder | The builder pattern (source.toBuilder().foo(value).build()) |
@ViaConstructor | The all-args constructor |
@ViaCopyAndSet | A copy constructor followed by a setter call (legacy bean style) |
Traversal hints
| Annotation | Generates |
|---|---|
@ThroughField | A traversal composing a lens-to-field with the auto-detected container traversal |
@TraverseWith | A 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.
| Annotation | Apply to | Effect |
|---|---|---|
@TraverseField | record 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 record | Get/set fields functionally | @GenerateLenses |
| A record | Navigate deeply with a fluent DSL | @GenerateLenses + @GenerateFocus |
A record with a List field | Update every element | @GenerateTraversals |
| A record | Query without modifying | @GenerateFolds |
A sealed interface or enum | Operate on one variant | @GeneratePrisms |
| Two equivalent types | Convert 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 tricky | Custom optics with copy strategy | @ImportOptics on an OpticsSpec interface + spec-method hints |
| A domain record and a wire DTO | Map both ways, with validation | @GenerateMapping on a MappingSpec interface |
| A domain record and a PATCH request bean | Fold the present fields into an update, leave the absent ones | @GenerateMapping on an UpdateSpec interface |
- The target tells you which annotation you want. A record takes
@GenerateLensesand friends, a sealed interface or enum takes@GeneratePrisms, a static method takes@GenerateIsos, and a type you do not own is reached through@ImportOpticsor anOpticsSpecinterface. - Annotations stack rather than compete. Each generates its own companion class, so a record carrying three of them gets three, and you pick the entry point that matches the task.
- The spec-method hints exist because external types have no single copy mechanism. Four of the eight say how to rebuild the object; two say how to narrow a variant; two say how to reach a container.
@TraverseFieldis the odd one out. It goes on a record component of your own, not on a spec method, which is why it has a section to itself.
- Quickstart: the same annotations doing real work, in three examples
- Optics for External Types: the
@ImportOpticsroute in full - Taming JSON with Jackson: the spec-interface route, and when to prefer it
- Record Mapping: the mapping and assembly generators in section 3
- Lens & Prism Journey: 40 minutes and 30 exercises, hands-on
Previous: Quickstart Next: Fundamentals