As Spring Boot 3 has raised the JDK requirement overnight to support at least Java 17, all code can now leverage newer language features.

For bean classes, one newer language that comes into play in this context is Java Records.

Some problems might appear when new mixes with the old, and one such example is mixing Records with good old (infamous) java.util.Optional.

How Optional was used before Records

Usually, Optional was used in code that also tries to be immutable:

@Value
public class Request {
    private final String optionalValue;
 
    public Optional<String> getOptionalValue() {
        return Optional.ofNullable(optionalValue);
    }
}

The reasoning is simple:

  1. In the wider community, using Optional as a field was considered a bad practice, but using it as a return type was considered valid use if used judiciously. There is, however, little consent on what counts as judicious, with some sources even claiming that it’s only allowed for JDK developers.
  2. It’s easier to integrate with various libraries, especially in the early days of Java 8 when many libraries didn’t have first-class support for Optional.

One thing to note – even though Lombok’s @Value  makes fields immutable, creates required properties constructor, getters, toString, and hashCode, in the instance above Lombok doesn’t generate a getter.

A lengthy opinion piece has been published on whether Lombok should even have first-class support for Optional or not (Language-Design:-Null-vs.-OptionalGitHub issue).

On the Optional controversy

Optional in Java 8 was quite controversial back in the day, with original creators going as far as saying this should only be used by JDK-provided APIs, meaning user code shouldn’t use Optional other than for local variables.

Over time this opinion has softened quite a bit, and a few years ago, some Oracle employees actually clearly expressed that you should use Optional over null wherever possible.

Still, all this controversy has warranted some against using it. And introduced some other, uninvited, to the discussion.

Java Records

Now, back to the people in the real world, trying to solve real problems…

If one tried to apply @Value pattern of only using Optional on a getter, the end result, when combined with Records, would probably look something like this:

public record Request(String optionalValue) {
 
    public Optional<String> optionalValue() {
        return Optional.ofNullable(optionalValue);
    }
}

and they would probably have gotten away with it if it hadn’t been for that meddling compiler:

java: invalid accessor method in record com.infobip.message.viber.Request
 
  (return type of accessor method optionalValue() must match the type of record component optionalValue)

In plain English – the input type for optionalValue must be the same for a constructor and accessor (`optionalValue()`).

Show me the code

So the only way to use Optional with Records is to declare the property to be of type Optional:

public record Request(Optional<String> optionalValue) {
}

But I don’t want to pass Optional as an argument

There’s little discussion about this issue – a constructor must accept the same type as accessor returns.

Now before you start typing custom constructors or static factory methods to hide the fact, there is a framework that alleviates the issue somewhat. Still, I’d advise only using it when necessary (meaning many optional parameters):

Record builder

Record builder is yet another annotation processor that automates the creation of Record builder with some additional bells and whistles.

Usually, I use it like this:

@RecordBuilder.Template(options = @RecordBuilder.Options(
    addConcreteSettersForOptional = true
))
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
@Inherited
public @interface CommonRecordBuilder {
 
}

Now Request call site can use code like this:

// given
String value = null;
var builder = RequestBuilder.builder();
 
// when
builder.optionalValue(value);
 
// then
then(builder.build()).isEqualTo(new Request(Optional.empty()));

and not deal with Optional parameters.

Jackson support

Jackson has supported Java Records for over two years now, since version 2.12.0. Needless to say, if you’re on Spring Boot 3, you’re probably on a newer version of Jackson.

Bean Validation

Bean validation is usually used by annotating fields. Since Bean Validation doesn’t work the same with Optional<T> as with just T, there are certain changes that need to be done:

@Value
public class Request {
    @Size(max = 100)
    private final String optionalValue;
 
    public Optional<String> getOptionalValue() {
        return Optional.ofNullable(optionalValue);
    }
}
public record Request(Optional<@Size(max = 100) String> optionalValue) {
 
}

An important bit is that @Size is inlined into the generic type signature.

OpenAPI

OpenAPI, Bean Validation, Records, and Optional currently don’t play well together. Or, to be more correct, Springdoc doesn’t.

In short, Springdoc doesn’t correctly recognize Optional as a wrapper type. I expect Springdoc maintainers will fix the issue.

A way forward

Based on the current Java roadmap, it’s clear that the future is going more in a direction toward the Records, with value (or inline) types being one of the goals down that road.

The old way of defining classes with Lombok’s @Value  is quickly becoming obsolete and should be avoided wherever possible.