# Optics Quickstart

_Change a field three records deep in one line, then a list, a sealed type and a Jackson tree._

~~~admonish info title="What You'll Learn"
- Change a field three records deep with one generated path
- Apply one change to every element of a list inside a record
- Match one variant of a sealed type, and move to another without a `modify`
- Generate optics for a type you cannot annotate, such as Jackson's `JsonNode`
~~~

~~~admonish example title="See Example Code"
**The code on this page is [QuickstartBook.java](https://github.com/higher-kinded-j/higher-kinded-j/blob/main/hkj-examples/src/main/java/org/higherkindedj/example/book/optics/quickstart/QuickstartBook.java) and its [QuickstartBookTest.java](https://github.com/higher-kinded-j/higher-kinded-j/blob/main/hkj-examples/src/test/java/org/higherkindedj/example/book/optics/quickstart/QuickstartBookTest.java)**: the page includes them directly, so the build compiles and runs them.
~~~

Your project needs Java 25 and the annotation processor `hkj-processor`, which the HKJ build plugin wires in; the [chapter introduction](ch_intro.md) sets both out.

You do **not** need to understand higher-kinded types, profunctors, or `Applicative` to use any of the code on this page. The annotations generate plain Java classes you call with familiar method chains.

---

## 1. Annotate, then update

The chapter's running cast is an order service. Annotate each record:

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

``` java
@GenerateLenses
@GenerateFocus
public record Customer(String name, EmailAddress email) {}
```

``` java
@GenerateLenses
@GenerateFocus
public record EmailAddress(String value) {}
```

Then a field three records deep, the customer's email address, is one line:

``` java
    Order updated = OrderFocus.customer().email().value().set("ada@example.org", order);
```

The annotation processor runs at compile time and produces `OrderLenses`, `OrderFocus`, `CustomerLenses`, `CustomerFocus`, `EmailAddressLenses` and `EmailAddressFocus` for you, and `OrderTraversals` for the list of lines. There is no reflection at runtime; the path you wrote is just a chain of typed method calls.

Each of those methods returns a *lens*: a record's accessor paired with a copy that replaces that one component through the canonical constructor. A path joins lenses end to end, so a write three records down rebuilds each record on the way, and reuses everything off the path.

~~~admonish example title="What the processor wrote" collapsible=true
A generated lens class is plain Java. This one comes from the processor's own tests, for a four-field `Customer(id, name, email, loyaltyPoints)` rather than the cast's: it holds one static method per component and a `withX` helper per component. The processor's tests compare this file line for line against what it generates. It was generated through `@ImportOptics`, which is why it has a package of its own; `@GenerateLenses` writes the same class into the record's package.

``` java
// Generated by hkj-optics-processor. Do not edit.
package com.test.optics;

import com.test.Customer;
import java.lang.Integer;
import java.lang.String;
import org.higherkindedj.optics.Lens;
import org.higherkindedj.optics.annotations.Generated;

/**
 * Generated optics for {@link Customer}. Do not edit.
 */
@Generated
public final class CustomerLenses {
  private CustomerLenses() {
  }

  /**
   * Creates a {@link Lens} for the {@code id} field of a {@link Customer}.
   *
   * @return A non-null {@code Lens<Customer, String>}.
   */
  public static Lens<Customer, String> id() {
    return Lens.of(Customer::id, (source, newValue) -> new Customer(newValue, source.name(), source.email(), source.loyaltyPoints()));
  }

  /**
   * Creates a {@link Lens} for the {@code name} field of a {@link Customer}.
   *
   * @return A non-null {@code Lens<Customer, String>}.
   */
  public static Lens<Customer, String> name() {
    return Lens.of(Customer::name, (source, newValue) -> new Customer(source.id(), newValue, source.email(), source.loyaltyPoints()));
  }

  /**
   * Creates a {@link Lens} for the {@code email} field of a {@link Customer}.
   *
   * @return A non-null {@code Lens<Customer, String>}.
   */
  public static Lens<Customer, String> email() {
    return Lens.of(Customer::email, (source, newValue) -> new Customer(source.id(), source.name(), newValue, source.loyaltyPoints()));
  }

  /**
   * Creates a {@link Lens} for the {@code loyaltyPoints} field of a {@link Customer}.
   *
   * @return A non-null {@code Lens<Customer, Integer>}.
   */
  public static Lens<Customer, Integer> loyaltyPoints() {
    return Lens.of(Customer::loyaltyPoints, (source, newValue) -> new Customer(source.id(), source.name(), source.email(), newValue));
  }

  /**
   * Creates a new {@link Customer} instance with an updated {@code id} field.
   * <p>This is a convenience method that uses the {@link #id()} lens.
   *
   * @param source The original {@code Customer} instance.
   * @param newId The new value for the {@code id} field.
   * @return A new, updated {@code Customer} instance.
   */
  public static Customer withId(Customer source, String newId) {
    return id().set(newId, source);
  }

  /**
   * Creates a new {@link Customer} instance with an updated {@code name} field.
   * <p>This is a convenience method that uses the {@link #name()} lens.
   *
   * @param source The original {@code Customer} instance.
   * @param newName The new value for the {@code name} field.
   * @return A new, updated {@code Customer} instance.
   */
  public static Customer withName(Customer source, String newName) {
    return name().set(newName, source);
  }

  /**
   * Creates a new {@link Customer} instance with an updated {@code email} field.
   * <p>This is a convenience method that uses the {@link #email()} lens.
   *
   * @param source The original {@code Customer} instance.
   * @param newEmail The new value for the {@code email} field.
   * @return A new, updated {@code Customer} instance.
   */
  public static Customer withEmail(Customer source, String newEmail) {
    return email().set(newEmail, source);
  }

  /**
   * Creates a new {@link Customer} instance with an updated {@code loyaltyPoints} field.
   * <p>This is a convenience method that uses the {@link #loyaltyPoints()} lens.
   *
   * @param source The original {@code Customer} instance.
   * @param newLoyaltyPoints The new value for the {@code loyaltyPoints} field.
   * @return A new, updated {@code Customer} instance.
   */
  public static Customer withLoyaltyPoints(Customer source, int newLoyaltyPoints) {
    return loyaltyPoints().set(newLoyaltyPoints, source);
  }
}
```
~~~

~~~admonish tip title="Why two annotations?"
`@GenerateLenses` produces classic lenses (`OrderLenses.customer()`) plus `withFoo` helpers for shallow updates. `@GenerateFocus` adds the path-based DSL (`OrderFocus.customer()`) for deep navigation, and its navigators let the next hop chain off it as `.email()` rather than `.via(CustomerFocus.email())`. Most records benefit from both annotations.
~~~

---

## 2. Sum types and collections, the same way

Collection fields and sealed interfaces use the same annotation-driven pattern. An order's lines are a `List<LineItem>`:

``` java
@GenerateLenses
@GenerateFocus
public record LineItem(String sku, Integer quantity, BigDecimal price) {}
```

**Round every price to pence:**

``` java
    Order rounded =
        OrderFocus.lines()
            .price()
            .modifyAll(price -> price.setScale(2, RoundingMode.HALF_EVEN), order);
```

`OrderFocus.lines()` already walks every element of the `List<LineItem>`: the generated accessor ends in `.each(EachInstances.listEach())` for you, so it hands back a path focused on a `LineItem`, not on the list. It is a navigator, as `customer()` is in section 1, so `.price()` zooms each element down to the price field. `modifyAll` applies the function in one pass to return a new `Order`. The introduction's discount left the lines at 36.000 and 2.250; rounding gives 36.00 and 2.25.

~~~admonish warning title="Do not add your own `each`"
`OrderFocus.lines().each(EachInstances.listEach())` does not compile: the path already focuses a `LineItem`, and a `LineItem` is not a `List`. The deprecated no-argument `.each()` does compile, because its element type is inferred from whatever you assign it to, and then fails at run time with a `ClassCastException`.
~~~

**Match a variant, and move to another.** A consignment's state is a sealed interface, and `@GeneratePrisms` writes one prism per variant:

``` java
/** Where a consignment has got to. */
@GeneratePrisms
public sealed interface ConsignmentState
    permits ConsignmentState.Pending, ConsignmentState.Dispatched, ConsignmentState.Returned {

  record Pending() implements ConsignmentState {}

  @GenerateFocus
  record Dispatched(Instant at) implements ConsignmentState {}

  @GenerateFocus
  record Returned(String reason) implements ConsignmentState {}
}
```

``` java
@GenerateLenses
@GenerateFocus
public record Consignment(UUID orderId, Address to, ConsignmentState state) {}
```

``` java
    boolean isPending = ConsignmentStatePrisms.pending().matches(consignment.state());

    // modify rebuilds the variant it narrowed to, so the function is Returned -> Returned
    ConsignmentState tidied =
        ConsignmentStatePrisms.returned()
            .modify(
                returned -> new ConsignmentState.Returned(returned.reason().strip()),
                consignment.state());

    // Moving to a different variant is not a modify. Read through the prism, then build.
    ConsignmentState dispatched =
        ConsignmentStatePrisms.pending()
            .getOptional(consignment.state())
            .<ConsignmentState>map(_ -> new ConsignmentState.Dispatched(dispatchedAt))
            .orElse(consignment.state());
```

A `Prism` is the sum-type counterpart of a lens: it succeeds when the variant matches and is a no-op otherwise. Note what `modify` will and will not do. Its function is `A -> A`, so it rebuilds the *same* variant; changing `Pending` into `Dispatched` is a read followed by a build, not a modification.

~~~admonish note title="Two views of the same record"
We added three annotations to `Order`. They don't conflict; each generates its own companion class (`OrderLenses`, `OrderFocus`, `OrderTraversals`) and you pick the entry point that matches your task.
~~~

---

## 3. Annotating types you don't own

External types like JDK classes, Jackson nodes, JOOQ records, and Protobuf messages can't be annotated directly. Higher-Kinded-J solves this with `OpticsSpec`: you declare the optics you want as an interface, and the processor generates them. Here is one for Jackson 3's `JsonNode`:

``` java
@ImportOptics
public interface JsonNodeOpticsSpec extends OpticsSpec<JsonNode> {

  @InstanceOf(ObjectNode.class)
  Prism<JsonNode, ObjectNode> object();

  @InstanceOf(ArrayNode.class)
  Prism<JsonNode, ArrayNode> array();

  @InstanceOf(StringNode.class)
  Prism<JsonNode, StringNode> text();

  @InstanceOf(NumericNode.class)
  Prism<JsonNode, NumericNode> numeric();

  @InstanceOf(BooleanNode.class)
  Prism<JsonNode, BooleanNode> bool();
}
```

The processor reads the spec and generates a `JsonNodeOptics` class (the `Spec` suffix is dropped) with one prism per method, each backed by an `instanceof` pattern match:

``` java
    JsonNode response = new ObjectMapper().readTree(json);

    Optional<ArrayNode> items = JsonNodeOptics.array().getOptional(response.get("items"));

    Optional<StringNode> firstName =
        items
            .flatMap(array -> JsonNodeOptics.object().getOptional(array.get(0)))
            .flatMap(object -> JsonNodeOptics.text().getOptional(object.get("name")));
```

~~~admonish tip title="Plain Java wins here"
For one read like this, Jackson's own `response.at("/items/0/name")` is shorter. The prisms earn their place when they compose with the rest of your optics, so that a JSON tree joins the same paths as your records. [Taming JSON with Jackson](optics_spec_interfaces.md) shows that, and how `@MatchWhen` handles Jackson's predicate-based type checks.
~~~

~~~admonish tip title="You can ship now"
You can now change a field three records deep, every element of a list, and one variant of a sealed type, through paths the processor generates. The rest of the chapter is for when a task needs more, starting with the Focus DSL in full.
~~~

~~~admonish question title="Checkpoint: what `set` leaves behind" id="check-quickstart-set"
After this line, what does `order.customer().email().value()` hold, and is `updated.lines()` the same list as `order.lines()`?

``` java
    Order updated = OrderFocus.customer().email().value().set("ada@example.org", order);
```
~~~

~~~admonish success title="Answer and why" collapsible=true id="check-quickstart-set-answer"
**The old address, `ada@example.com`, and yes.** `set` changes nothing in place: it returns a new `Order`, rebuilding each record on the path and reusing everything off it, the lines included.

Where this lives: [Annotate, then update](#1-annotate-then-update).
~~~

~~~admonish question title="Checkpoint: `modify` on another variant" id="check-quickstart-prism"
A consignment is `Pending`. What does `ConsignmentStatePrisms.returned().modify(_ -> new ConsignmentState.Returned("lost"), consignment.state())` return?
~~~

~~~admonish success title="Answer and why" collapsible=true id="check-quickstart-prism-answer"
**The `Pending` state, unchanged.** The prism focuses only a `Returned`, so on any other variant `modify` has nothing to change, and hands the value back as it was. Moving a consignment to another variant is a read through the prism followed by a build.

Where this lives: [Sum types and collections, the same way](#2-sum-types-and-collections-the-same-way).
~~~

---

~~~admonish info title="Key Takeaways"
* **The annotations generate plain Java at compile time.** `XLenses`, `XFocus`, `XPrisms` and `XTraversals` are ordinary classes you call with ordinary method chains. Nothing here uses runtime reflection.
* **`@GenerateLenses` and `@GenerateFocus` are the pair to reach for.** Lenses give you `withFoo` and the classic optics; Focus gives you the path DSL, whose navigators let a hop into another record chain as a method call.
* **A collection accessor is already element-level.** `OrderFocus.lines()` focuses a `LineItem`, because the generated method ends in `.each(EachInstances.listEach())` for you. Adding another does not compile.
* **A prism rebuilds the variant it narrowed to.** `modify` is `A -> A`, so it cannot turn a `Pending` into a `Dispatched`; that is a read through the prism followed by building the new variant.
* **Types you do not own join the same vocabulary.** An `OpticsSpec` interface plus `@ImportOptics` generates optics for `JsonNode`, JOOQ records or JDK types, and they compose with everything else.
~~~

---

~~~admonish tip title="See Also"
- [Annotations at a Glance](annotations_at_a_glance.md): every `@Generate*` and spec hint, with its target and what it produces
- [Focus DSL](focus_dsl.md): the path-based API this page previews, in full
- [Decision Trees](decision_trees.md#tree-2-which-api-style): choosing between the Focus DSL, the Fluent API and the Free Monad DSL
- [What a Path Is Made Of](optics_intro.md): the optics a path wraps, and when you need one
- [Record Mapping](../mapping/ch_intro.md): the domain to wire boundary, which needs none of this chapter first
~~~

~~~admonish info title="Hands-On Learning"
Practise this page on the same order in [Tutorial 00: Your First Path](https://github.com/higher-kinded-j/higher-kinded-j/blob/main/hkj-examples/src/test/java/org/higherkindedj/tutorial/optics/Tutorial00_FirstPath.java) (3 exercises), which also checks that your build is set up.
~~~

~~~admonish tip title="Ready for hands-on?"
The [Optics Tutorial Track](../tutorials/optics/ch_intro.md) (210 exercises) is exercise-driven. Six journeys run from Lens & Prism through the Focus DSL to batching, coupled updates, and the generated DTO boundary. Recommended once you've finished this Quickstart.
~~~

---

**Previous:** [Optics](ch_intro.md)
**Next:** [Focus DSL](focus_dsl.md)
