Nullability Annotations
- What Are Nullability Annotations?
- How Vaadin Uses Nullability Annotations
- Enabling Null Checking in Your Project
- Nullability with Signals
- Best Practices
- Related Topics
Vaadin uses JSpecify annotations to express nullability contracts in its APIs. These annotations indicate whether method parameters, return types, and generic type arguments can be null. Combined with a static analysis tool like NullAway, they catch potential NullPointerException errors at compile time.
What Are Nullability Annotations?
Java has no built-in way to express whether a reference can be null. JSpecify fills this gap with a standard set of annotations:
@NullMarked-
Marks a class or package as having non-null types by default. All unannotated type usages within the scope are treated as non-null.
@Nullable-
Explicitly marks a type as potentially
null. Used on parameters, return types, and type arguments that may legitimately benull.
Together, these annotations express nullability contracts across three areas:
Source code
Java
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
public class UserService {
// Return type: non-null by default
public String getName() {
return "John";
}
// Parameter: explicitly nullable
public void setNickname(@Nullable String nickname) {
// nickname can be null
}
// Type argument: nullable generic
public ValueSignal<@Nullable String> optionalName() {
return new ValueSignal<>(null);
}
}How Vaadin Uses Nullability Annotations
Vaadin’s Signal APIs are annotated with JSpecify nullability annotations. All signal types use the bounded type parameter pattern <T extends @Nullable Object>, which allows callers to choose whether a signal holds nullable or non-null values:
Source code
Java
// Non-null signal: get() returns String
ValueSignal<String> name = new ValueSignal<>("John");
// Nullable signal: get() returns @Nullable String
ValueSignal<@Nullable String> optionalName = new ValueSignal<>(null);Nullability annotations are expected to expand across more Vaadin APIs in the future.
Enabling Null Checking in Your Project
Vaadin’s APIs are annotated with JSpecify nullability information. To take advantage of this, enable NullAway with Error Prone in your project. This gives you compile-time errors when you misuse Vaadin’s nullability contracts — for example, passing null where a non-null parameter is expected, or ignoring a @Nullable return value. This requires JDK 22 or later.
Maven Setup
Use the nullability-maven-plugin to configure Error Prone and NullAway automatically:
Source code
XML
<plugin>
<groupId>am.ik.maven</groupId>
<artifactId>nullability-maven-plugin</artifactId>
<version>0.3.0</version>
<extensions>true</extensions>
<executions>
<execution>
<goals>
<goal>configure</goal>
</goals>
</execution>
</executions>
</plugin>The plugin configures Error Prone and NullAway for you. By default, it enables JSpecify mode and checks all code in @NullMarked scopes.
Gradle Setup
For Gradle, use the gradle-errorprone-plugin:
Source code
groovy
plugins {
id "net.ltgt.errorprone" version "4.1.0"
}
dependencies {
errorprone "com.google.errorprone:error_prone_core:2.36.0"
errorprone "com.uber.nullaway:nullaway:0.12.6"
}
tasks.withType(JavaCompile).configureEach {
options.errorprone {
disableAllChecks = true
error("NullAway")
option("NullAway:JSpecifyMode", "true")
option("NullAway:AnnotatedPackages", "com.example")
}
}Checking Vaadin Types
After adding the tooling, mark classes that use Vaadin APIs with @NullMarked. NullAway then enforces Vaadin’s nullability contracts in those classes at compile time:
Source code
Java
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
public class SignalExample extends Div {
// Compiler ensures non-null signal is never set to null
private final ValueSignal<String> name = new ValueSignal<>("John");
// Compiler requires null checks when reading nullable signals
private final ValueSignal<@Nullable String> nickname = new ValueSignal<>(null);
public SignalExample() {
@Nullable String value = nickname.get();
// Compiler error if you call value.toUpperCase() without a null check
add(new Span(value != null ? value : "No nickname"));
}
}Extending to Your Own Code
You can go further and apply @NullMarked at the package level to get null-safety defaults for your own APIs. Create a package-info.java file:
Source code
Java
@NullMarked
package com.example.myapp;
import org.jspecify.annotations.NullMarked;All classes in the package are then non-null by default. Use @Nullable only where null is a valid value:
Source code
Java
package com.example.myapp;
import org.jspecify.annotations.Nullable;
public class CustomerService {
// Non-null return type (default)
public Customer findById(long id) { ... }
// Explicitly nullable return type
public @Nullable Customer findByEmail(String email) { ... }
}Nullability with Signals
Signal types use <T extends @Nullable Object>, so the nullability of the value depends on the type argument you provide.
Non-Null and Nullable Signals
By default, signals hold non-null values:
Source code
Java
ValueSignal<String> nameSignal = new ValueSignal<>("John");
String name = nameSignal.get(); // Never nullTo allow null values, annotate the type argument with @Nullable:
Source code
Java
import org.jspecify.annotations.Nullable;
ValueSignal<@Nullable String> optionalName = new ValueSignal<>(null);
@Nullable String value = optionalName.get(); // May be null
if (value != null) {
System.out.println(value.toUpperCase());
}Handling Null in Signal Operations
When transforming nullable signals, account for null values:
Source code
Java
ValueSignal<@Nullable String> input = new ValueSignal<>(null);
// Transform to non-null
Signal<String> output = input.map(str -> str != null ? str.toUpperCase() : "");When binding nullable signals to components, convert null to a suitable default:
Source code
Java
ValueSignal<@Nullable String> optionalText = new ValueSignal<>(null);
TextField field = new TextField();
field.bindValue(
optionalText.map(text -> text != null ? text : ""),
value -> optionalText.set(value.isEmpty() ? null : value)
);Collection Type Nullability
Nullability applies independently to a collection and its elements:
Source code
Java
// Non-null list, non-null elements
ValueSignal<List<String>> names = new ValueSignal<>(List.of("a", "b"));
// Non-null list, nullable elements
ValueSignal<List<@Nullable String>> sparse = new ValueSignal<>(new ArrayList<>());
// Nullable list, non-null elements
ValueSignal<@Nullable List<String>> optionalList = new ValueSignal<>(null);Best Practices
-
Prefer non-null by default. Use
@Nullableonly whennullis a meaningful value in your domain, not as a convenience. -
Apply
@NullMarkedat the package level. This provides consistent defaults and reduces annotation noise across your codebase. -
Handle null explicitly in transformations. When mapping or binding nullable signals, convert to non-null values early rather than propagating null through chains of operations.