Feb 24, 2026
Feb 24, 2026
N/A Views
MD

What is JSpecify?

JSpecify is an open-source project aimed at standardizing nullability annotations for Java.

In the Java ecosystem, annotations such as @Nullable and @NonNull have historically been defined separately by multiple libraries like JetBrains, FindBugs, and Checker Framework, leading to compatibility issues. JSpecify unifies these and aims to provide a specification that can be used consistently across tools.
Around 2006, JSR‑305 (effectively discontinued) was proposed as a JCP standard for software defect detection annotations, but JSpecify covers areas that JSR‑305 fell short on, such as handling nullability for type arguments and generics.

The org.jspecify.annotations package defines annotations such as @Nullable, @NonNull, and @NullMarked, enabling static analysis tools and IDEs to recognize them for more accurate null checks.

Spring Framework 7 supports JSpecify and requires explicit @Nullable on methods that may return null or parameters that may accept null. Otherwise, null returns and null assignments are not permitted. See this blog post for details.

Using JSpecify annotations allows IDEs to warn about code lacking proper null checks. However, on its own, it does not cause compilation errors.

What is NullAway?

NullAway is a tool that detects Null Pointer Exceptions in Java via static analysis. It runs as an Error Prone plugin and reports null-safety violations as warnings or errors during the build. It recognizes JSpecify and JSR‑305 annotations and adopts an opt-out approach where references without @Nullable are considered non‑null, making incremental adoption into existing codebases easier.

Combining NullAway with JSpecify enables compilation errors for code that lacks proper null checks.

For example, compile the following code with NullAway enabled.

package com.example.foo;

import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;

@Component
public class Foo {

    private final RestClient restClient;

    public Foo(RestClient.Builder restClientBuilder) {
        this.restClient = restClientBuilder.build();
    }

    public String getFoo() {
        return this.restClient.get().uri("http://example.com/foo").retrieve().body(String.class);
    }

}

This results in the following compilation error.

[ERROR] /Users/toshiaki/.../Foo.java:[16,2] error: [NullAway] returning @Nullable expression from method with @NonNull return type

The body method of RestClient is annotated with @Nullable as shown below, indicating it may return null, yet the code returns it without any annotation or check.

        /**
         * Extract the body as an object of the given type.
         * @param bodyType the type of return value
         * @param <T> the body type
         * @return the body, or {@code null} if no response body was available
         * @throws RestClientResponseException by default when receiving a
         * response with a status code of 4xx or 5xx. Use
         * {@link #onStatus(Predicate, ErrorHandler)} to customize error response
         * handling.
         */
        <T> @Nullable T body(Class<T> bodyType);

You can either write code that handles the null case, or annotate the method with @Nullable to propagate the possibility of null, thereby eliminating the compilation error.

package com.example.foo;

import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;

import java.util.Objects;

@Component
public class Foo {

    private final RestClient restClient;

    public Foo(RestClient.Builder restClientBuilder) {
        this.restClient = restClientBuilder.build();
    }

    public String getFoo() {
        return Objects.requireNonNull(this.restClient.get().uri("http://example.com").retrieve().body(String.class),
                "Response body must not be null");
    }

}

Note that to treat unannotated elements as non‑null by default, you need to add the @NullMarked annotation to the class or package. In the above example, a package-info.java like the following is provided.

@NullMarked
package com.example.foo;

import org.jspecify.annotations.NullMarked;

Using JSpecify/NullAway with Maven

So far we have seen that using JSpecify/NullAway enforces null checks. How can we integrate this into Maven?

The following configuration is required for the maven-compiler-plugin (requires JDK 22+).

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.15.0</version>
        <configuration>
          <annotationProcessorPaths>
            <path>
              <groupId>com.google.errorprone</groupId>
              <artifactId>error_prone_core</artifactId>
              <version>2.47.0</version>
            </path>
            <path>
              <groupId>com.uber.nullaway</groupId>
              <artifactId>nullaway</artifactId>
              <version>0.13.1</version>
            </path>
          </annotationProcessorPaths>
          <fork>true</fork>
          <compilerArgs>
            <arg>-XDcompilePolicy=simple</arg>
            <arg>--should-stop=ifError=FLOW</arg>
            <!-- @formatter:off -->
            <arg>-Xplugin:ErrorProne -XepDisableAllChecks -XepOpt:NullAway:OnlyNullMarked=true -XepOpt:NullAway:JSpecifyMode=true -Xep:NullAway:ERROR -XepExcludedPaths:(.*/test/java/.*|.*/target/generated-sources/.*)</arg>
            <!-- @formatter:on -->
            <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED</arg>
            <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED</arg>
            <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED</arg>
            <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED</arg>
            <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED</arg>
            <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED</arg>
            <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>
            <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>
            <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg>
            <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED</arg>
          </compilerArgs>
        </configuration>
      </plugin>

You could copy and paste this, but managing it across many Maven projects becomes cumbersome…

Introducing the Nullability Maven Plugin

The Nullability Maven Plugin is a Maven plugin that removes this boilerplate from your pom.xml.

https://github.com/making/nullability-maven-plugin

Using the Nullability Maven Plugin, the above configuration can be simplified as follows.

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.15.0</version>
</plugin>
<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>

Any configuration you have already set for maven-compiler-plugin will be automatically merged.

Tip

You can view the merged configuration with mvn help:effective-pom.

While not a limitation of the plugin itself, using NullAway's JSpecify Mode requires compiling with JDK 22 or newer, though the target can be 17. If you must compile with an older JDK (17 or 21), see this page.

Automatic Generation of package-info.java

One hassle when using JSpecify is creating package-info.java. A package-info.java with @NullMarked must be created for each subpackage, and it's easy to forget.

By default, the Nullability Maven Plugin treats classes or packages lacking @NullMarked as an error. Additionally, you can configure it to automatically generate package-info.java during the build.

<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>
                <goal>generate-package-info</goal><!-- added -->
            </goals>
        </execution>
    </executions>
</plugin>

See here for configuring the output directory.


We have shown how Maven users can easily adopt JSpecify / NullAway using the Nullability Maven Plugin. Give it a try.

Found a mistake? Update the entry.
Share this article: