This version is still in development and is not considered stable yet. For the latest stable version, please use Spring Framework 6.2.7! |
Null-safety
Although Java does not let you express nullness markers with its type system yet, the Spring Framework codebase is annotated with JSpecify annotations to declare the nullability of its APIs, fields, and related type usages. Reading the JSpecify user guide is highly recommended in order to get familiar with those annotations and semantics.
The primary goal of this null-safety arrangement is to prevent a NullPointerException
from being thrown at runtime via build
time checks and to use explicit nullability as a way to express the possible absence of value. It is useful in both
Java by leveraging some tooling (NullAway or IDEs supporting JSpecify annotations
such as IntelliJ IDEA) and Kotlin where JSpecify annotations are automatically translated to
Kotlin’s null safety.
The Nullness
Spring API can be used at runtime to detect the nullness of a
type usage, a field, a method return type, or a parameter. It provides full support for JSpecify annotations,
Kotlin null safety, and Java primitive types, as well as a pragmatic check on any @Nullable
annotation (regardless of the
package).
Annotating libraries with JSpecify annotations
As of Spring Framework 7, the Spring Framework codebase leverages JSpecify annotations to expose null-safe APIs and to check the consistency of those nullability declarations with NullAway as part of its build. It is recommended for each library depending on Spring Framework and Spring portfolio projects, as well as other libraries related to the Spring ecosystem (Reactor, Micrometer, and Spring community projects), to do the same.
Leveraging JSpecify annotations in Spring applications
Developing applications with IDEs that support nullness annotations will provide warnings in Java and errors in Kotlin
when the nullability contracts are not honored, allowing Spring application developers to refine their null handling to
prevent a NullPointerException
from being thrown at runtime.
Optionally, Spring application developers can annotate their codebase and use build plugins like NullAway to enforce null-safety at the application level during build time.
Guidelines
The purpose of this section is to share some proposed guidelines for explicitly specifying the nullability of Spring-related libraries or applications.
JSpecify
The key points to understand are that the nullness of types is unknown in Java by default and that non-null type
usage is by far more frequent than nullable usage. In order to keep codebases readable, we typically want to define
by default that type usage is non-null unless marked as nullable for a specific scope. This is exactly the purpose of
@NullMarked
which is typically set in Spring
projects at the package level via a package-info.java
file, for example:
@NullMarked
package org.springframework.core;
import org.jspecify.annotations.NullMarked;
In the various Java files belonging to the package, nullable type usage is defined explicitly with
@Nullable
. It is recommended that this
annotation is specified just before the related type on the same line.
For example, for a field:
private @Nullable String fileEncoding;
Or for method parameters and method return types:
public static @Nullable String buildMessage(@Nullable String message,
@Nullable Throwable cause) {
// ...
}
When overriding a method, JSpecify annotations are not inherited from the original method. That means the JSpecify annotations should be copied to the overriding method if you want to override the implementation and keep the same nullability semantics. |
With arrays and varargs, you need to be able to differentiate the nullness of the elements from the nullness of the array itself. Pay attention to the syntax defined by the Java specification which may be initially surprising:
-
@Nullable Object[] array
means individual elements can be null but the array itself cannot. -
Object @Nullable [] array
means individual elements cannot be null but the array itself can. -
@Nullable Object @Nullable [] array
means both individual elements and the array can be null.
The Java specification also enforces that annotations defined with @Target(ElementType.TYPE_USE)
like JSpecify
@Nullable
should be specified after the last .
with inner or fully qualified types:
-
Cache.@Nullable ValueWrapper
-
jakarta.validation.@Nullable Validator
@NonNull
and
@NullUnmarked
should rarely be needed for
typical use cases.
NullAway
Configuration
The recommended configuration is:
-
NullAway:OnlyNullMarked=true
in order to perform nullability checks only for packages annotated with@NullMarked
. -
NullAway:CustomContractAnnotations=org.springframework.lang.Contract
which makes NullAway aware of the @Contract annotation in theorg.springframework.lang
package which can be used to express complementary semantics to avoid irrelevant warnings in your codebase.
A good example of the benefits of a @Contract
declaration can be seen with
Assert.notNull()
which is annotated
with @Contract("null, _ → fail")
. With that contract declaration, NullAway will understand that the value passed as a
parameter cannot be null after a successful invocation of Assert.notNull()
.
Optionally, it is possible to set NullAway:JSpecifyMode=true
to enable
checks on the full JSpecify semantics, including annotations on
generic types. Be aware that this mode is
still under development and requires
using JDK 22 or later (typically combined with the --release
Java compiler flag to configure the
expected baseline). It is recommended to enable the JSpecify mode only as a second step, after making sure the codebase
generates no warning with the recommended configuration mentioned previously in this section.
Warnings suppression
There are a few valid use cases where NullAway will incorrectly detect nullability problems. In such case, it is recommended to suppress related warnings and to document the reason:
-
@SuppressWarnings("NullAway.Init")
at field, constructor, or class level can be used to avoid unnecessary warnings due to the lazy initialization of fields – for example, due to a class implementingInitializingBean
. -
@SuppressWarnings("NullAway") // Dataflow analysis limitation
can be used when NullAway dataflow analysis is not able to detect that the path involving a nullability problem will never happen. -
@SuppressWarnings("NullAway") // Lambda
can be used when NullAway does not take into account assertions performed outside of a lambda for the code path within the lambda. -
@SuppressWarnings("NullAway") // Reflection
can be used for some reflection operations that are known to return non-null values even if that cannot be expressed by the API. -
@SuppressWarnings("NullAway") // Well-known map keys
can be used whenMap#get
invocations are performed with keys that are known to be present and when non-null related values have been inserted previously. -
@SuppressWarnings("NullAway") // Overridden method does not define nullability
can be used when the superclass does not define nullability (typically when the superclass comes from a dependency).
Migrating from Spring null-safety annotations
Spring null-safety annotations @Nullable
,
@NonNull
,
@NonNullApi
, and
@NonNullFields
in the org.springframework.lang
package were
introduced in Spring Framework 5 when JSpecify did not exist, and the best option at that time was to leverage
meta-annotations from JSR 305 (a dormant but widespread JSR). They are deprecated as of Spring Framework 7 in favor of
JSpecify annotations, which provide significant enhancements such as properly
defined specifications, a canonical dependency with no split-package issues, better tooling, better Kotlin integration,
and the capability to specify nullability more precisely for more use cases.
A key difference is that Spring’s deprecated null-safety annotations, which follow JSR 305 semantics, apply to fields, parameters, and return values; while JSpecify annotations apply to type usage. This subtle difference is in practice pretty significant, as it allows developers to differentiate between the nullness of elements and the nullness of arrays/varargs as well as to define the nullness of generic types.
That means array and varargs null-safety declarations have to be updated to keep the same semantics. For example
@Nullable Object[] array
with Spring annotations needs to be changed to Object @Nullable [] array
with JSpecify
annotations. The same applies to varargs.
It is also recommended to move field and return value annotations closer to the type and on the same line, for example:
-
For fields, instead of
@Nullable private String field
with Spring annotations, useprivate @Nullable String field
with JSpecify annotations. -
For method return types, instead of
@Nullable public String method()
with Spring annotations, usepublic @Nullable String method()
with JSpecify annotations.
Also, with JSpecify, you do not need to specify @NonNull
when overriding a type usage annotated with @Nullable
in the
super method to "undo" the nullable declaration in null-marked code. Just declare it unannotated and the null-marked
defaults (a type usage is considered non-null unless explicitly annotated as nullable) will apply.