Let's get started with a Microservice Architecture with Spring Cloud:
A Practical Guide to Null-Safety in Java With JSpecify
Last updated: November 19, 2025
1. Introduction
One of the common sources of frustration for Java developers is the NullPointerException. Be it working in large code bases or making an API call, Java developers always had to question themselves, “What if this returns null?” and how to handle it. Despite Java being a statically typed language, its handling of null values has always left room for ambiguity.
In the recent past, the Java Community has taken steps towards addressing this very issue. One of the promising developments in this area is JSpecify.
In this tutorial, let’s explore what JSpecify is and how we can implement it in our projects.
2. What Is JSpecify?
Jspecify provides a standard set of annotations to explicitly declare the nullness expectations of the Java code. Jspecify is tool-agnostic, meaning it’s not tied to any specific framework or IDE. It works across the entire Java ecosystem.
It allows developers to annotate if a method, field, or parameter (including generic parameters) can hold a null value or not. This helps IDEs, static analysis tools, and compilers catch potential null-related issues during development.
While there were annotations for null checks in the past, the problem was that different projects and tools often used different annotations with slightly varying meanings. JSpecify, however, seeks to unify these efforts under one precise, consistent, and interoperable standard.
3. Why Care About Null Safety?
Historically, Java relied on implicit nullability, where variables are assumed to be nullable or non-nullable based on default behavior or context, without the developer explicitly specifying it every time.
If the tools are aware of the null expectations, they can warn us when we violate them. This helps us catch bugs early in the development process.
Also, when we explicitly specify nullability, consumers of our API immediately understand whether a method can return null or whether a parameter can accept null. Their IDE shows nullability information as hints or warnings.
4. How to Use JSpecify
JSpecify allows us to specify various annotations to express nullness.
To start using Jspecify, we need to add the following dependency:
<dependency>
<groupId>org.jspecify</groupId>
<artifactId>jspecify</artifactId>
<version>1.0.0</version>
</dependency>
JSpecify offer various annotations to express nullness.
The @Nullable annotation means that the annotated element may legally be null. @NonNull means that the annotated element must never be null.
We can also specify nullness at the package or class level. For example, the @NullMarked annotation is applied to a package, class, or module to indicate that, by default, all unannotated types are considered non-null.
Similarly, we have @NullUnmarked annotation, which cancels the effect of @NullMarked and allows unannotated types to have unspecified nullness.
These annotations provide flexibility. For example, we can make everything non-null by default by using @NullMarked and only explicitly annotate places where null is acceptable with @Nullable. This way, we can reduce the number of annotations required.
Once we add the dependency, we can start using the annotations in our code as follows:
@Nullable
private String findNicknameOrNull(String userId) {
if ("user123".equals(userId)) {
return "CoolUser";
} else {
return null;
}
}
@Test
void givenUnknownUserId_whenFindNicknameOrNull_thenReturnsNull() {
String nickname = findNicknameOrNull("unknownUser");
assertNull(nickname);
}
@Test
void givenNullableMethodResult_whenWrappedInOptional_thenHandledSafely() {
String nickname = findNicknameOrNull("unknownUser");
Optional<String> safeNickname = Optional.ofNullable(nickname);
assertTrue(safeNickname.isEmpty());
}
In the above code, the method findNicknameOrNull(String userId) is annotated with @Nullable, which signals to the developer that it might return null. This helps catch potential null-related issues at compile time. However, since JSpecify has no effect during runtime, the test here verifies the expected runtime behavior where the method indeed returns null when the user is not found.
In the second test, though, we’re using Optional to safely encapsulate the null value, thereby eliminating the risk of a NullPointerException.
5. Comparison with Other Null-Checking Approaches
Before JSpecify, we had other approaches to check for nullability. In this section, we’ll explore various ways to write null-safe code.
5.1. Using Optional
java.util.Optional is a container object that wraps the return value that either contains a non-null value or signifies absence. It provides a type-safe and explicit way to handle the potential absence of a value, thereby reducing the risk of a NullPointerException. Optional forces the caller of the method to consciously handle both scenarios.
For example, let’s take a look at the code below:
private Optional<String> findNickname(String userId) {
if ("user123".equals(userId)) {
return Optional.of("CoolUser");
} else {
return Optional.empty();
}
}
The method never returns null. It always returns an Optional. Either Optional.of(value) when there is a value. Optional.empty() when there isn’t a value.
Now, let’s take a look at how we deal with Optional from the calling method:
@Test
void givenKnownUserId_whenFindNickname_thenReturnsOptionalWithValue() {
Optional<String> nickname = findNickname("user123");
assertTrue(nickname.isPresent());
assertEquals("CoolUser", nickname.get());
}
@Test
void givenUnknownUserId_whenFindNickname_thenReturnsEmptyOptional() {
Optional<String> nickname = findNickname("unknownUser");
assertTrue(nickname.isEmpty());
}
We can see from the above code that the caller will either receive the Optional with a value or an empty Optional and must handle presence or absence explicitly, which reduces the risk of getting a NullPointerException.
However, Optional is primarily intended for method return types and not for fields or method parameters. Using Optional for fields or method parameters would introduce additional complexity.
Also, Optional introduces a small performance cost due to object creation and wrapping.
5.2. Using Objects.requireNonNull()
Another common practice for handling potential null-related issues is by using runtime assertions, using Objects.requireNonNull. This method throws a NullPointerException immediately if the provided argument is null.
For example, let’s take a look at this code:
@Test
void givenNonNullArgument_whenValidate_thenDoesNotThrowException() {
String result = processNickname("CoolUser");
assertEquals("Processed: CoolUser", result);
}
@Test
void givenNullArgument_whenValidate_thenThrowsNullPointerException() {
assertThrows(NullPointerException.class, () -> processNickname(null));
}
private String processNickname(String nickname) {
Objects.requireNonNull(nickname, "Nickname must not be null");
return "Processed: " + nickname;
}
As we can see from the above code, if the argument is null, a NullPointerException is thrown immediately. It makes bugs more visible during testing as they fail early instead of silently flowing into deeper logic.
By validating the inputs right at the beginning of a method, we can catch violations during development or testing; this will prevent errors during production.
However, one key limitation of requireNonNull() is that it only detects problems during runtime. They do not provide compile-time or IDE hints like JSpecify.
6. JSpecify Adoption Strategy
It’s impractical to annotate the entire codebase in one go. Thankfully, JSpecify allows gradual adoption. We can incrementally implement null safety annotations without breaking the code.
A typical adoption strategy is to start annotating small, self-contained packages or classes and then use @NullMarked to enforce non-null defaults, reducing annotation noise. We can then explicitly add @Nullable annotations wherever necessary.
We can also run static analysis tools to catch any mismatches and refine our annotations, and then gradually expand coverage to broader parts of the codebase.
7. Tooling and Ecosystem Support
Many of the popular tools and IDEs now support JSpecify annotations to varying degrees.
For example, Checker Framework, which is a popular static analysis tool, has its own annotations for null safety; however, it has started supporting JSpecify’s core annotations in recent versions.
Similarly, NullAway is yet another static analysis tool focused on detecting nullability issues. It now supports JSpecify annotations.
From the IDE’s front, IntelliJ IDEA has long been supporting nullness annotations, including its own. IntelliJ now offers basic recognition of JSpecify annotations, highlighting mismatches and potential nullability problems.
8. Conclusion
In this article, we discussed the JSpecify tool that helps developers get rid of null-related errors to a great degree. It certainly makes the Java codebase more robust and resilient, with fewer surprises during production. While tooling support is still maturing, the momentum behind JSpecify suggests it will soon become the default approach for expressing nullness in Java.
The code backing this article is available on GitHub. Once you're logged in as a Baeldung Pro Member, start learning and coding on the project.

















