# Optics for External Types

_Generate optics for a JDK or library type you cannot annotate, and compose them with your own._

> *"The real voyage of discovery consists not in seeking new landscapes, but in having new eyes."*
>
> – Marcel Proust

---

The landscape (JDK classes, database libraries, JSON parsers) already exists. What changes is how we *see* it. With `@ImportOptics` we gain new eyes: `LocalDate`, `JsonNode` or any external type becomes a participant in compositional, type-safe transformations. We are not adding code to those libraries; we are generating a view onto them.

~~~admonish info title="What You'll Learn"
- Generate optics for a JDK or library type with `@ImportOptics` on a `package-info.java`
- Predict what auto-detection generates for a record, a sealed type, an enum, a wither class and a container field
- Check the pairing rule that decides whether a `withX` method gets a lens
- Compose an imported optic with your own, such as the year an order settled
- Decide when to write a spec interface instead, for builders, predicates or non-standard naming
~~~

---

## The Frustration

Optics work beautifully across your own records. Then you hit a type you do not own:

<!-- verify -->
```java
// An order's settlement: our own record, dated with the JDK's LocalDate
@GenerateLenses
record Settlement(UUID orderId, LocalDate settledOn, BigDecimal amount) {}

// How do we bump just the year inside settledOn?
// LocalDate lives in java.time. We cannot annotate it.
```

**This is what `@ImportOptics` solves.**

---

## The Quick Win

Put the annotation on a `package-info.java` in your optics package:

```java
@ImportOptics(java.time.LocalDate.class)
package com.myapp.optics;

import org.higherkindedj.optics.annotations.ImportOptics;
```

The processor analyses `LocalDate`, finds the *wither* methods it can pair with a getter (`getX()` reads, `withX(value)` returns a modified copy), and generates `LocalDateLenses`. Now external and local optics compose as equals:

<!-- verify -->
```java
// settledOn() is ours (@GenerateLenses); year() is the JDK's (@ImportOptics)
Settlement nextYear =
    SettlementLenses.settledOn().andThen(LocalDateLenses.year()).modify(y -> y + 1, settlement);
// the same settlement, dated a year later

int year = SettlementLenses.settledOn().andThen(LocalDateLenses.year()).get(settlement);
```

One annotation, and a JDK type joins the pipeline.

~~~admonish tip title="Why this matters"
Nothing here is reflective and nothing is a string. `LocalDateLenses.year()` is a generated `Lens<LocalDate, Integer>` built from `getYear()` and `withYear(int)`, so it composes with your own optics, obeys the lens laws, and fails at compile time if the library changes under you. The alternative, hand-writing `Lens.of(LocalDate::getYear, LocalDate::withYear)` for every field of every external type, is the same code you would have written, minus the typing.
~~~

---

## How Auto-Detection Works

The processor looks at each imported type and picks a strategy from its shape:

```mermaid
flowchart LR
    accTitle: How auto-detection picks a strategy
    accDescr: A record gets lenses through its canonical constructor, a sealed interface one prism per permitted subtype, an enum one prism per constant, and a class with withX methods lenses through getter and wither. Any other type is not supported, and needs a spec interface.
    T{"The imported<br/>type is..."}
    T -->|"a record"| R(["Lenses, via the<br/>canonical constructor"])
    T -->|"a sealed interface"| S@{ shape: hex, label: "Prisms, one per<br/>permitted subtype" }
    T -->|"an enum"| E@{ shape: hex, label: "Prisms, one per<br/>constant" }
    T -->|"a class with<br/>withX methods"| W(["Lenses, via<br/>getter and wither"])
    T -->|"anything else"| X["Not supported:<br/>write a spec interface"]

    classDef decision fill:#e5c890,stroke:#df8e1d,color:#232634
    classDef rw fill:#a6d189,stroke:#40a02b,color:#232634
    classDef error fill:#e78284,stroke:#d20f39,color:#232634
    class T decision
    class R,S,E,W rw
    class X error
```

In words: records and classes with withers get lenses, sealed interfaces and enums get prisms, and any other shape needs a spec interface.

### Records to Lenses

<!-- verify -->
```java
// The external library has:
public record Coordinate(double lat, double lon) {}

// You get:
Lens<Coordinate, Double> lat = CoordinateLenses.lat();
Lens<Coordinate, Double> lon = CoordinateLenses.lon();
```

Records are the easy case: the canonical constructor is the copy mechanism.

### Sealed Types to Prisms

<!-- verify -->
```java
// From a payment gateway's SDK, which we cannot annotate:
public sealed interface PaymentInstrument permits CardInstrument, BankAccount, CryptoWallet {}

Prism<PaymentInstrument, CardInstrument> card = PaymentInstrumentPrisms.cardInstrument();
Prism<PaymentInstrument, BankAccount> bankAccount = PaymentInstrumentPrisms.bankAccount();
Prism<PaymentInstrument, CryptoWallet> wallet = PaymentInstrumentPrisms.cryptoWallet();
```

These are the prisms `@GeneratePrisms` writes, so a permitted subtype that is an inner class of a generic class is named as [Generating the Prisms](prisms.md#step-1-generating-the-prisms) describes.

### Enums to Prisms

<!-- verify -->
```java
// From a courier's SDK, which we cannot annotate:
public enum TrackingStatus { PENDING, IN_TRANSIT, DELIVERED, RETURNED }

Prism<TrackingStatus, TrackingStatus> pending = TrackingStatusPrisms.pending();
Prism<TrackingStatus, TrackingStatus> inTransit = TrackingStatusPrisms.inTransit(); // and so on, one per constant
```

Each prism takes the constant's camelCase name, and a name that would be a Java keyword or literal takes a trailing underscore, as [Prisms](prisms.md) explains.

### Wither Classes to Lenses

Immutable JDK types and many library types follow the wither pattern: `getX()` reads, `withX(value)` returns a modified copy.

~~~admonish warning title="The pairing rule, exactly"
A lens is generated for `withXxx(T)` only when the type also has a public no-arg method named `xxx()`, `getXxx()` or `isXxx()` **returning exactly `T`**, and `withXxx` itself returns the type under its own type arguments, or a subtype of it. That is stricter than it looks. `LocalDate` gets `year()`, `dayOfMonth()` and `dayOfYear()`, but *not* a month lens: `withMonth` takes an `int`, while `getMonth()` returns `Month`, so the pair does not typecheck and the field is skipped. A wither on a `Tagged<T>` that returns a raw `Tagged`, or a `Tagged<String>`, is skipped the same way, since the lens hands its result back as a `Tagged<T>`; a retag, `<U> Tagged<U> withId(String)`, pairs, because the call infers `U` back to `T`.

The lens is named after what follows `with`, and how that is spelt decides what pairs:

| The class declares | `@ImportOptics` generates |
|---|---|
| `withdraw(long)` beside `getDraw()` | No `draw` lens. A lower-case letter straight after `with` makes a word, not a wither |
| `witheMail(String)` beside `geteMail()`, as Lombok's `beanspec` capitalisation writes them | An `eMail()` lens, as `withEMail(String)` beside `getEMail()` gives. A capital after the lower-case letter makes it a wither, and its getter may be spelt the same way |
| `withEMail(String)` beside only `geteMail()` | No lens. A `geteMail()` pairs only with a wither spelt the same way, `witheMail` |
| `withURL(String)` beside `getURL()` | A `uRL()` lens: the name is what follows `with`, its first letter in lower case. To name it `url()`, declare it in a [spec interface](optics_spec_interfaces.md) with `@Wither(value = "withURL", getter = "getURL")` |
| `with2D(int)` beside `get2D()` | No lens: it would be named `2D()`, and no method's name can start with a digit |
| `withN(int)` beside `n()`, and `withN(String)` beside `getN()` | One `n()` lens, through `withN(int)`. The rule prefers `n()`, then `getN()`, then `isN()`, and a note reports the other wither |
| `withEMail(String)` and `witheMail(String)`, both beside `eMail()` | One `eMail()` lens, through `withEMail`, whichever the class declares first. A note reports the other wither |

The generated lens calls the wither that paired, even where an overload of its name would take the boxed value the lens holds: a `withN(int)` beside a `withN(Integer)` or a `withN(Object)` is called as `source.withN((int) newValue)`, so the boxed overload does not take the call. When a wither you expected is missing from the generated class, the pairing rule is almost always why. Where the getter is the mismatch, reach for a [spec interface](optics_spec_interfaces.md) and name the getter yourself. Where the return is, a spec over the type the wither does return, `OpticsSpec<Tagged<String>>`, can use it; a raw return cannot be used by either route.
~~~

An inner class of a generic class is imported under its enclosing class's type parameters, since that is how the type has to be written once the enclosing class takes arguments: `@ImportOptics({Outer.Line.class})` on a `class Outer<X>` gives `<X> Lens<Outer<X>.Line, String>`.

---

## Container Fields Get Traversals

When an imported record has a collection field, you get both a lens to the collection and a traversal into its elements, named `<field>Traversal`:

<!-- verify -->
```java
// External:
public record Department(String name, List<Employee> staff) {}

// Generated:
Lens<Department, String> name = DepartmentLenses.name();
Lens<Department, List<Employee>> staff = DepartmentLenses.staff();
Traversal<Department, Employee> eachMember = DepartmentLenses.staffTraversal();
```

An element type written as a wildcard is focused as the type it stands for, as `@GenerateTraversals` does: `List<? extends Employee>` gives a `Traversal<Department, Employee>`, and `List<?>` a `Traversal<Department, Object>`. See [Wildcard Element Types](traversals.md#wildcard-element-types).

---

## A Real Workflow: Fiscal Year Normalisation

Composing across the boundary reads the same as composing within it:

<!-- verify -->
```java
// The year of the settlement date, as one optic
Lens<Settlement, Integer> settlementYear =
    SettlementLenses.settledOn().andThen(LocalDateLenses.year());

Settlement normalised = settlementYear.set(2027, settlement);
boolean inFiscalYear = settlementYear.get(settlement) == 2026;

// The generated wither helpers are there too, when a lens is more than you need
LocalDate monthStart = LocalDateLenses.withDayOfMonth(settlement.settledOn(), 1);
```

`settledOn().andThen(year())` reads as English: the year of the settlement date. Local and external optics are the same kind of value.

---

## When Auto-Detection Is Not Enough

Some types resist it:

**Builder patterns.** No withers, no all-args constructor. JOOQ POJOs, Lombok `@Builder`, Immutables, AutoValue, Protobuf messages all copy through a builder, and there is no naming convention the processor can assume.

**Non-standard naming.** `config.derivedWith(newValue)` rather than `withX`, a getter whose return type does not match the wither parameter (the `LocalDate.getMonth()` case above), or a wither that returns the type under other arguments.

**Predicate-based type discrimination.** Jackson's `JsonNode` uses `isObject()` and `isArray()` rather than a sealed hierarchy, so there is nothing to enumerate.

For these, declare what you want explicitly with a **spec interface**: an interface extending `OpticsSpec<S>` whose methods carry annotations telling the processor how to build each optic.

---

~~~admonish note title="Quick Reference"
```java
// Simple import: auto-detection handles the rest
@ImportOptics({
  java.time.LocalDate.class,
  java.time.LocalTime.class,
  com.library.SimpleRecord.class
})
package com.myapp.optics;

// The options, when you need them
@ImportOptics(
    value = {MutableConfig.class},
    allowMutable = true,                    // acknowledge the lens-law limitation
    targetPackage = "com.myapp.generated")
```

`@ImportOptics` goes on a `package-info.java` or on a type declaration; both generate into the annotated element's package unless `targetPackage` says otherwise.
~~~

---

## Choosing an Approach for a New Library

1. **Can you annotate the type?** It is your code: use `@GenerateLenses` and friends directly.
2. **Is it a record, sealed type, enum, or a wither class whose getters line up?** `@ImportOptics`, and you are done.
3. **Does it use builders, predicates, or non-standard naming?** Write a [spec interface](optics_spec_interfaces.md) and declare the optics you want.
4. **Does it already implement `List`, `Map` or `Optional`?** You may need nothing at all: the standard traversals work on it directly.

A type another annotation processor writes in the same build works like any other. List it, or name it as a spec's source type, and `@ImportOptics` waits until that processor has written it. If it never appears, javac reports the missing type, and code calling the generated class reports that class missing too.

---

~~~admonish info title="Key Takeaways"
* **`@ImportOptics` brings types you do not own into the same optic algebra as your own.** The generated optics compose with `andThen` exactly like the ones generated from your own records.
* **Four shapes are auto-detected**: records and wither classes give lenses, sealed types and enums give prisms.
* **The wither rule is strict about types.** `withX(T)` needs a getter returning exactly `T`, which is why `LocalDate` has no month lens, and has to return the class under its own type arguments, a subtype of it, or a retag the call infers back to it.
* **Collection fields get a traversal too**, named `<field>Traversal`.
* **Builders and predicate-based types need a spec interface**, which is the subject of the next two pages.
~~~

~~~admonish tip title="See Also"
- [Taming JSON with Jackson](optics_spec_interfaces.md): spec interfaces, `@InstanceOf` and `@MatchWhen`, worked on `JsonNode`
- [Database Records with JOOQ](copy_strategies.md): `@ViaBuilder` and the other copy strategies
- [Focus DSL with External Libraries](focus_external_bridging.md): bridging Focus navigation into generated external optics
~~~

~~~admonish tip title="Further Reading"
- **Oracle**: [java.time API](https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/time/package-summary.html): `LocalDate`, `LocalTime`, `Instant` and friends, the canonical wither-pattern types
- **Immutables**: [immutables.github.io](https://immutables.github.io/): value objects with generated builders and withers
- **AutoValue**: [github.com/google/auto](https://github.com/google/auto/tree/main/value): google's immutable value types
~~~

---

**Previous:** [Optics Extensions](optics_extensions.md)
**Next:** [Taming JSON with Jackson](optics_spec_interfaces.md)
