> ## Content Index
> Fetch the complete content index at: https://www.ggorantala.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# How to Use Optional in Java Without the Common Pitfalls
- URL: https://www.ggorantala.dev/how-to-use-optional-java/
- Published: 2026-10-21T05:19:00.000Z
- Updated: 2026-10-31T06:32:05.000Z
- Description: Replace nested null checks with Optional in a support-ticket service, then see which orElse, get and field habits bite. Java 21 code you can run today.
- Author: Gopi Gorantala
- Tags: how-to-tutorials, core-java, java-tutorials

## What you'll build: a phone lookup that can't throw

Here's the output of the finished program. Four tickets go in: one with a phone number, one whose agent has no number, one nobody owns, and one that doesn't exist. The old null-check code and the new `Optional` code run side by side.

```log
ticket 1  legacy=555-0101  optional=Optional[555-0101] call=555-0101
ticket 2  legacy=null      optional=Optional.empty call=555-9999
ticket 3  legacy=null      optional=Optional.empty call=555-9999
ticket 99 legacy=null      optional=Optional.empty call=555-9999
on-call supplier ran 3 times for 4 lookups

```

That last line is the point of half this article. Four lookups, and the fallback ran three times, not four. Get the `orElse` / `orElseGet` choice wrong and it runs four times. For a phone number that's harmless. For a database call or a remote service it isn't.

This is how to use Optional in Java the way the JDK authors meant it: as a return type that says "there might be nothing here", and not as a wrapper you sprinkle everywhere. You'll build a small support-ticket service, replace a pyramid of `if (x != null)` with one chain, and then break things on purpose so you know the traps by sight.

```log
TicketRepository.findById(id) --> Optional<Ticket>
        |  flatMap(Ticket::assignedAgent)  --> Optional<Agent>
        |  flatMap(Agent::phone)           --> Optional<String>
        v
   orElseGet(onCallPhone)  /  orElseThrow(...)  /  ifPresentOrElse(...)

```

This is for you if you know Java and have seen `Optional` in other people's code, but you've never been sure when `get()` is acceptable (almost never) or what `orElse` really does.

## What you need

| Tool                     | Version I used                                  | Check it with            |
| ------------------------ | ----------------------------------------------- | ------------------------ |
| JDK                      | OpenJDK 21.0.12.1                               | java -version            |
| Build                    | plain javac (Maven pom.xml below, not executed) | javac -version           |
| IntelliJ IDEA (optional) | 2026.2 line                                     | Help → About             |
| Eclipse IDE (optional)   | current release package "for Java Developers"   | Help → About Eclipse IDE |

You need Java 17 or newer for the code as written, because it uses records (Java 16). Everything in `Optional` itself works back to Java 8, apart from the methods I flag in the version notes. If you don't have a JDK, install one with SDKMAN (`sdk install java 21.0.x-tem`, pick the exact id from `sdk list java`) or from your OS package manager.

No Docker or Podman this time. Nothing here needs a database or a container, so I left them out.

## Spin up the project

I used the plain command line for this one, because there's nothing to download and the checks run in under a second. IDE steps follow.

```bash
mkdir tickets && cd tickets
mkdir -p src/main/java/com/example/tickets src/test/java/com/example/tickets

```

The tree you'll end up with:

```log
tickets/
├── pom.xml                      (only if you use Maven)
└── src/
    ├── main/java/com/example/tickets/
    │   ├── Agent.java
    │   ├── Main.java
    │   ├── Ticket.java
    │   ├── TicketRepository.java
    │   └── TicketService.java
    └── test/java/com/example/tickets/
        └── TicketChecks.java

```

### In IntelliJ IDEA

Choose File → New → Project, pick **Java** on the left, set the build system to **IntelliJ** (or Maven), and select JDK 21\. Name it `tickets` and create it. Right-click `src` → New → Package and create `com.example.tickets`. *I verified these menu paths from the JetBrains documentation. I didn't click through them.*

### In Eclipse

Choose File → New → Java Project, name it `tickets`, and set the execution environment to JavaSE-21\. Then right-click `src` → New → Package. *Docs only; not executed.*

### With Maven

Maven isn't needed to run any of this, and I couldn't download plugins from Maven Central in my environment, so I didn't execute this file. It has no dependencies, and I'd expect it to work as-is:

`pom.xml`

```xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.example</groupId>
  <artifactId>tickets</artifactId>
  <version>1.0.0</version>
  <properties>
    <maven.compiler.release>21</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
</project>

```

## Wire it up: from null checks to one chain

### Step 1: Model the data and say what's optional

**Goal:** make "this field might be missing" visible in the types.

An `Agent` always has a name. The phone number might not exist. A `Ticket` might have no assignee. I'm using records here (see [how to use Java records](https://ggorantala.dev/how-to-use-java-records/?ref=ggorantala.dev)) to keep the data classes short.

`src/main/java/com/example/tickets/Agent.java`

```java
package com.example.tickets;

import java.util.Optional;

public record Agent(String name, String phoneNumber) {

    public Agent {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("name is required");
        }
    }

    /** The phone number is optional, so the accessor says so in its type. */
    public Optional<String> phone() {
        return Optional.ofNullable(phoneNumber).map(String::strip).filter(s -> !s.isEmpty());
    }
}

```

`src/main/java/com/example/tickets/Ticket.java`

```java
package com.example.tickets;

import java.util.Optional;

public record Ticket(long id, String subject, Agent assignee) {

    public Optional<Agent> assignedAgent() {
        return Optional.ofNullable(assignee);
    }
}

```

Why: the record components stay plain `String` and `Agent` (nullable), and the *accessor-style methods* `phone()` and `assignedAgent()` hand back `Optional`. I'd rather not make the component itself an `Optional`; more on that in the traps. The `phone()` method also strips whitespace and treats a blank string as missing, so every caller gets the same answer.

**Checkpoint:** compile from the project root.

```bash
mkdir -p out
javac --release 21 -d out $(find src -name '*.java')

```

Nothing printed means success. If you add the other files first and compile early, you'll get `cannot find symbol` errors until `TicketService` exists; that's expected.

### Step 2: Return Optional from the lookup

**Goal:** stop returning `null` from `findById`.

`src/main/java/com/example/tickets/TicketRepository.java`

```java
package com.example.tickets;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

public class TicketRepository {

    private final Map<Long, Ticket> store = new HashMap<>();

    public void save(Ticket ticket) {
        store.put(ticket.id(), ticket);
    }

    /** "Might not exist" is part of the signature, not a comment. */
    public Optional<Ticket> findById(long id) {
        return Optional.ofNullable(store.get(id));
    }
}

```

Why: `Optional.ofNullable` is the bridge between old null-returning APIs (here, `Map.get`) and your new signature. Use `of` only when you know the value isn't null; it throws on null, which we'll trigger later.

### Step 3: Replace the null-check pyramid with a chain

**Goal:** write the lookup twice, once the old way and once with `Optional`, so you can compare them.

`src/main/java/com/example/tickets/TicketService.java`

```java
package com.example.tickets;

import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;

public class TicketService {

    private final TicketRepository repo;
    private final Supplier<String> onCallPhone;

    public TicketService(TicketRepository repo, Supplier<String> onCallPhone) {
        this.repo = repo;
        this.onCallPhone = onCallPhone;
    }

    /** The null-check version most of us have written (and then regretted). */
    public String assigneePhoneLegacy(long id) {
        Ticket t = repo.findById(id).orElse(null);
        if (t != null) {
            Agent a = t.assignee();
            if (a != null) {
                String p = a.phoneNumber();
                if (p != null && !p.isBlank()) {
                    return p.strip();
                }
            }
        }
        return null;
    }

    /** Same lookup as one chain; the result still says "might be empty". */
    public Optional<String> assigneePhone(long id) {
        return repo.findById(id)
                .flatMap(Ticket::assignedAgent)
                .flatMap(Agent::phone);
    }

    /** Fall back to the on-call phone, computed only when it's needed. */
    public String phoneToCall(long id) {
        return assigneePhone(id).orElseGet(onCallPhone);
    }

    /** Callers who can't continue without a ticket get a clear exception. */
    public Ticket requireTicket(long id) {
        return repo.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("No ticket with id " + id));
    }

    /** Try the assignee's phone, then the team lead's, before giving up. */
    public Optional<String> phoneWithFallback(long id, Agent teamLead) {
        return assigneePhone(id).or(teamLead::phone);
    }

    /** Names of everyone who owns at least one of these tickets. */
    public List<String> assigneeNames(List<Long> ids) {
        return ids.stream()
                .map(repo::findById)
                .flatMap(Optional::stream)
                .flatMap(t -> t.assignedAgent().stream())
                .map(Agent::name)
                .distinct()
                .toList();
    }

    public String describe(long id) {
        return repo.findById(id)
                .filter(t -> t.assignee() != null)
                .map(t -> "#" + t.id() + " " + t.subject() + " -> " + t.assignee().name())
                .orElse("#" + id + " is missing or unassigned");
    }
}

```

Read `assigneePhoneLegacy` and `assigneePhone` next to each other. They return the same answers (the tests prove it later), but the second one has no variables, no nesting, and no way to forget a check. The tools in play:

- `map` transforms the value if one is present. Use it when your function returns a plain value.
- `flatMap` is `map` for functions that already return an `Optional`. Using `map` there gives you `Optional<Optional<Agent>>`, which compiles and is useless.
- `orElseGet(supplier)` computes a fallback only when the `Optional` is empty.
- `orElseThrow(supplier)` turns "empty" into an exception with a message you chose.
- `or(supplier)` (Java 9+) tries another `Optional` before giving up.
- `Optional::stream` (Java 9+) turns an `Optional` into a stream of zero or one elements, which is the tidy way to drop the empties from a list.
- `filter` empties the `Optional` when the predicate fails.

### Step 4: Add a main method

**Goal:** see all four cases at once.

`src/main/java/com/example/tickets/Main.java`

```java
package com.example.tickets;

import java.util.List;

public class Main {
    public static void main(String[] args) {
        TicketRepository repo = new TicketRepository();
        repo.save(new Ticket(1, "Login loop", new Agent("Priya", " 555-0101 ")));
        repo.save(new Ticket(2, "Refund missing", new Agent("Marcus", null)));
        repo.save(new Ticket(3, "Typo on invoice", null));

        int[] onCallCalls = {0};
        TicketService svc = new TicketService(repo, () -> { onCallCalls[0]++; return "555-9999"; });
        Agent lead = new Agent("Dana", "555-0200");

        for (long id : new long[] {1, 2, 3, 99}) {
            System.out.printf("ticket %-2d legacy=%-9s optional=%-14s call=%s%n",
                    id, svc.assigneePhoneLegacy(id), svc.assigneePhone(id), svc.phoneToCall(id));
        }
        System.out.println("on-call supplier ran " + onCallCalls[0] + " times for 4 lookups");
        System.out.println(svc.phoneWithFallback(2, lead));
        System.out.println(svc.assigneeNames(List.of(1L, 2L, 3L, 99L, 1L)));
        System.out.println(svc.describe(1));
        System.out.println(svc.describe(3));
        svc.assigneePhone(2).ifPresentOrElse(
                p -> System.out.println("calling " + p),
                () -> System.out.println("no phone for ticket 2, paging on-call"));
        try {
            svc.requireTicket(99);
        } catch (IllegalArgumentException e) {
            System.out.println("caught: " + e.getMessage());
        }
    }
}

```

**Checkpoint:** compile and run.

```bash
javac --release 21 -d out $(find src -name '*.java')
java -cp out com.example.tickets.Main

```

```log
ticket 1  legacy=555-0101  optional=Optional[555-0101] call=555-0101
ticket 2  legacy=null      optional=Optional.empty call=555-9999
ticket 3  legacy=null      optional=Optional.empty call=555-9999
ticket 99 legacy=null      optional=Optional.empty call=555-9999
on-call supplier ran 3 times for 4 lookups
Optional[555-0200]
[Priya, Marcus]
#1 Login loop -> Priya
#3 is missing or unassigned
no phone for ticket 2, paging on-call
caught: No ticket with id 99

```

If you see that, the hard part's done. Notice ticket 1: the stored number was `" 555-0101 "` with spaces, and both versions returned it stripped.

## Check it works

Walk through the output once, because each line is a rule.

- **Line 5, "ran 3 times for 4 lookups":** `orElseGet` only called the supplier for tickets 2, 3 and 99\. Ticket 1 had a phone, so the fallback never ran.
- **`Optional[555-0200]`:** ticket 2's agent has no number, so `or(teamLead::phone)` tried the team lead's instead.
- **`[Priya, Marcus]`:** ids 3 and 99 produced nothing, and the repeated id 1 was removed by `distinct()`. That's `flatMap(Optional::stream)` doing the filtering.
- **`#3 is missing or unassigned`:** `filter` emptied the `Optional` for the unassigned ticket, and `orElse` with a cheap string built the message.

Now prove that `orElse(expression)` really evaluates its argument eagerly. I'll do it in the tests.

## Test it

I couldn't reach Maven Central from my environment to pull JUnit, so these checks are a plain `main` method that throws on the first failure. Port each `check` to an `@Test` with AssertJ's `assertThat(...)` if you use JUnit 5 (a dedicated article is planned: [how to write unit tests with JUnit 5](https://ggorantala.dev/how-to-write-unit-tests-junit-5-java/?ref=ggorantala.dev)). The logic doesn't change.

`src/test/java/com/example/tickets/TicketChecks.java`

```java
package com.example.tickets;

import java.util.List;
import java.util.NoSuchElementException;
import java.util.Optional;

public class TicketChecks {
    static int passed = 0;

    static void check(String name, boolean ok) {
        if (!ok) throw new AssertionError("FAILED: " + name);
        passed++;
        System.out.println("ok  " + name);
    }

    public static void main(String[] args) {
        TicketRepository repo = new TicketRepository();
        repo.save(new Ticket(1, "Login loop", new Agent("Priya", " 555-0101 ")));
        repo.save(new Ticket(2, "Refund missing", new Agent("Marcus", null)));
        repo.save(new Ticket(3, "Typo on invoice", null));
        repo.save(new Ticket(4, "Blank phone", new Agent("Lee", "   ")));
        int[] calls = {0};
        TicketService svc = new TicketService(repo, () -> { calls[0]++; return "555-9999"; });
        Agent lead = new Agent("Dana", "555-0200");

        check("phone present and stripped", svc.assigneePhone(1).equals(Optional.of("555-0101")));
        check("null phone is empty", svc.assigneePhone(2).isEmpty());
        check("unassigned ticket is empty", svc.assigneePhone(3).isEmpty());
        check("missing ticket is empty", svc.assigneePhone(99).isEmpty());
        check("blank phone is empty", svc.assigneePhone(4).isEmpty());

        for (long id : new long[] {1, 2, 3, 4, 99}) {
            String legacy = svc.assigneePhoneLegacy(id);
            check("legacy matches optional for " + id, Optional.ofNullable(legacy).equals(svc.assigneePhone(id)));
        }

        calls[0] = 0;
        svc.phoneToCall(1);
        check("orElseGet does not call supplier when value present", calls[0] == 0);
        check("orElseGet falls back", svc.phoneToCall(3).equals("555-9999") && calls[0] == 1);

        calls[0] = 0;
        svc.assigneePhone(1).orElse(svc.phoneToCall(3));
        check("orElse(expr) evaluates expr even when value present", calls[0] == 1);

        check("or() uses lead", svc.phoneWithFallback(2, lead).equals(Optional.of("555-0200")));
        check("or() keeps assignee", svc.phoneWithFallback(1, lead).equals(Optional.of("555-0101")));
        check("distinct names via stream()", svc.assigneeNames(List.of(1L, 2L, 3L, 99L, 1L)).equals(List.of("Priya", "Marcus")));
        check("describe unassigned", svc.describe(3).equals("#3 is missing or unassigned"));

        try { svc.requireTicket(99); check("requireTicket throws", false); }
        catch (IllegalArgumentException e) { check("requireTicket message", e.getMessage().equals("No ticket with id 99")); }

        try { Optional.of(null); check("Optional.of(null) throws", false); }
        catch (NullPointerException e) { check("Optional.of(null) throws NPE", true); }

        try { Optional.empty().get(); check("get() throws", false); }
        catch (NoSuchElementException e) { check("get() on empty: " + e.getMessage(), e.getMessage().equals("No value present")); }

        check("Optional.equals compares contents", Optional.of("a").equals(Optional.of("a")) && Optional.empty().equals(Optional.empty()));
        System.out.println(passed + " checks passed");
    }
}

```

```bash
javac --release 21 -d out $(find src -name '*.java')
java -cp out com.example.tickets.TicketChecks

```

```log
ok  phone present and stripped
ok  null phone is empty
ok  unassigned ticket is empty
ok  missing ticket is empty
ok  blank phone is empty
ok  legacy matches optional for 1
ok  legacy matches optional for 2
ok  legacy matches optional for 3
ok  legacy matches optional for 4
ok  legacy matches optional for 99
ok  orElseGet does not call supplier when value present
ok  orElseGet falls back
ok  orElse(expr) evaluates expr even when value present
ok  or() uses lead
ok  or() keeps assignee
ok  distinct names via stream()
ok  describe unassigned
ok  requireTicket message
ok  Optional.of(null) throws NPE
ok  get() on empty: No value present
ok  Optional.equals compares contents
21 checks passed

```

The `legacy matches optional` checks run all five tickets through both implementations. That's the safety net for a refactor: if you're replacing null-check code in a real codebase, write that comparison first.

## How it works

`Optional<T>` is a final class holding either one non-null `T` or nothing. There's a single shared empty instance, which is why `Optional.empty() == Optional.empty()` printed `true` when I tried it. Don't lean on that: two `Optional.of("x")` calls give you different objects (`==` printed `false`), so compare with `equals`. `Optional.equals` compares the contents.

`map` and `flatMap` look inside the box. If the box is empty, your function never runs, and the empty result flows down the chain. That's why the chain in `assigneePhone` can't throw a `NullPointerException`: there's no point where a null is dereferenced. The `orElse` family is where the chain ends and you decide what "nothing" means: a default, a computed default, or an exception.

The design intent, from the Javadoc: `Optional` is for return types where "no result" is a normal outcome and `null` would likely cause errors. It isn't meant for fields, parameters, or collection elements. For the field-and-parameter reasoning in more depth, see [how to create an immutable class in Java](https://ggorantala.dev/how-to-create-immutable-class-java/?ref=ggorantala.dev), where I model optional data without `Optional` fields. If you want the `equals`/`hashCode` side of it, [how to implement equals() and hashCode() in Java](https://ggorantala.dev/how-to-implement-equals-hashcode-java/?ref=ggorantala.dev) covers it.

## When it doesn't work

These are all things I forced while building this, with the exact output from my run.

**`Optional.of(null)` throws.** Use `ofNullable` when the value might be null.

```log
java.lang.NullPointerException

```

**`get()` on an empty `Optional`.** The message is `No value present`, and it's an unchecked `NoSuchElementException`, so it shows up at runtime and not in review. Replace `get()` with `orElseThrow(...)` so the message says what actually went wrong (`No ticket with id 99`). Since Java 10 there's also a no-argument `orElseThrow()` that does the same as `get()` but says so in its name.

**The old chain on a null.** This is what you're moving away from. On Java 21 the helpful NPE message names the exact call:

```log
NPE: Cannot invoke "Traps$Agent.phone()" because the return value of "Traps$Ticket.a()" is null

```

**`Optional<Optional<Agent>>`.** If you used `map` where the function returns an `Optional`, the output looks like `Optional[Optional[a]]`. Switch to `flatMap`.

**Type errors from mixing the two worlds.** These compile errors are common when you're half-migrated:

```log
Bad.java:3: error: incompatible types: String cannot be converted to Optional<String>
  static Optional<String> f() { return "x"; }

```

Wrap the value with `Optional.of(...)` or `ofNullable(...)`.

**Optional as a field in a serialized class.** `Optional` doesn't implement `Serializable`:

```log
NotSerializable: java.util.Optional

```

That's by design. Keep the field nullable and expose an `Optional` getter, the way `Ticket.assignedAgent()` does. If you hit an `InvalidClassException` instead, that's a different problem: see [java.io.InvalidClassException: local class incompatible](https://ggorantala.dev/java-invalidclassexception-local-class-incompatible-serialversionuid/?ref=ggorantala.dev).

**Compiling for an old Java version.** I tried `--release 8` and `--release 11`. Both fail on the records, not on `Optional`:

```log
Agent.java:5: error: records are not supported in -source 8

```

If you're stuck on Java 8, replace the records with plain classes. And note which `Optional` methods you can't use there: `or`, `stream`, `ifPresentOrElse` (Java 9), `orElseThrow()` with no argument (Java 10), `isEmpty` (Java 11).

**`orElse` running your expensive code.** Not an error, which is why it's the worst one. `opt.orElse(loadFromDatabase())` calls `loadFromDatabase()` every time, even when `opt` has a value. My check `orElse(expr) evaluates expr even when value present` proves it. Use `orElseGet(() -> loadFromDatabase())`. For a constant, a literal, or a variable that's already computed, `orElse` is fine.

If your real problem is an NPE you can't explain, see [java.lang.NullPointerException: cannot invoke because null](https://ggorantala.dev/java-nullpointerexception-cannot-invoke-because-null/?ref=ggorantala.dev).

## Going further

Habits I'd keep:

- **Return `Optional`, don't accept it.** A method parameter of type `Optional<String>` forces the caller to wrap something and still allows `null` to be passed. Use two overloads or a nullable parameter with a clear Javadoc.
- **Never return `Optional<List<T>>`.** An empty list already means "nothing". I confirmed `Optional.of(List.of())` is present and empty at once (`true true`), which makes callers check twice.
- **Don't call `isPresent()` then `get()`.** That's a null check with extra steps. Use `map`, `ifPresent`, or `ifPresentOrElse`.
- **Skip it in hot loops and DTOs.** Each `Optional` is an allocation, and JSON libraries handle a nullable field more predictably than an `Optional` field. For primitives there are `OptionalInt`, `OptionalLong` and `OptionalDouble`.
- **Don't wrap `Optional.ofNullable(x).orElse(y)` around a single null check.** `x != null ? x : y` is clearer. Use `Objects.requireNonNullElse(x, y)` for the same thing without the allocation.

Version notes: `Optional` arrived in Java 8\. `or`, `stream`, `ifPresent`/`ifPresentOrElse` (Java 9), no-arg `orElseThrow()` (Java 10), `isEmpty` (Java 11). Java 21 gives you everything above. I only ran this on 21, so check the Javadoc if you're on a newer JDK.

Next in this series: [how to implement the strategy pattern in Java](https://ggorantala.dev/how-to-implement-strategy-pattern-java/?ref=ggorantala.dev) and [how to implement the builder pattern in Java](https://ggorantala.dev/how-to-implement-builder-pattern-java/?ref=ggorantala.dev). Build time: I built this in one sitting, and the slowest part was working out a clean way to prove that `orElse` evaluates eagerly (a counter in the supplier did it).

## Recap

- Return `Optional` from methods where "no result" is normal; use `ofNullable` to wrap legacy null returns.
- Chain with `map` for plain values and `flatMap` for `Optional`\-returning functions.
- End chains with `orElseGet`, `orElseThrow(...)` or `ifPresentOrElse`, never a bare `get()`.
- Use `orElseGet` whenever the fallback costs something.
- Don't use `Optional` for fields, parameters, collections or serialization.
- Before refactoring null-check code, write the old-versus-new comparison test.

## Optional questions people ask

**When should I use Optional in Java?**  
As a return type for a method that might legitimately find nothing, like a lookup by id. Avoid it for fields, method parameters and collection elements.

**What's the difference between orElse and orElseGet?**  
`orElse(x)` evaluates `x` before the call, every time. `orElseGet(supplier)` runs the supplier only when the `Optional` is empty. In my run the supplier executed 3 times for 4 lookups, where `orElse` would have executed 4.

**Should I ever call Optional.get()?**  
Rarely. It throws `NoSuchElementException: No value present` on empty. Prefer `orElseThrow(() -> new SomeException("clear message"))`, or the no-argument `orElseThrow()` on Java 10+.

**Can I use Optional in JPA entities or JSON classes?**  
Keep the field a plain nullable type and return `Optional` from the getter. `Optional` isn't `Serializable`, and persistence and JSON mapping handle plain nullable fields more predictably.

**How do I turn a list of Optionals into a list of values?**  
`list.stream().flatMap(Optional::stream).toList()` on Java 9+, which is what `assigneeNames` does.

## References

- [java.util.Optional, Java SE 21 API](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Optional.html?ref=ggorantala.dev)
- [JEP 395: Records](https://openjdk.org/jeps/395?ref=ggorantala.dev)
- [JEP 358: Helpful NullPointerExceptions](https://openjdk.org/jeps/358?ref=ggorantala.dev)
- IntelliJ IDEA release notes, JetBrains blog (2026.2 line, observed 2026-10-25)