NullAway Documentation

A tool to help eliminate NullPointerExceptions (NPEs) in your Java code with low build-time overhead

RAW Doc

Technical Documentation: uber/NullAway

โ„น๏ธ Provenance: Multi-Tier Ingestion ยท CodeWiki Reference

Overview (`README.md`)

NullAway: Fast Annotation-Based Null Checking for Java [](https://github.com/uber/nullaway/actions/workflows/continuous-integration.yml) [](https://codecov.io/github/uber/NullAway?branch=master)

NullAway is a tool to help eliminate NullPointerExceptions (NPEs) in your Java code. To use NullAway, first add @Nullable annotations in your code wherever a field, method parameter, or return value may be null. Given these annotations, NullAway performs a series of type-based, local checks to ensure that any pointer that gets dereferenced in your code cannot be null. NullAway is similar to the type-based nullability checking in the Kotlin and Swift languages, and the Checker Framework and Eradicate null checkers for Java.

NullAway is fast. It is built as a plugin to Error Prone and can run on every single build of your code. In our measurements, the build-time overhead of running NullAway is usually less than 10%. NullAway is also practical: it does not prevent all possible NPEs in your code, but it catches most of the NPEs we have observed in production while imposing a reasonable annotation burden, giving a great "bang for your buck."

Installation

Overview

NullAway requires that you build your code with JDK 17 or higher and Error Prone, version 2.36.0 or higher. See the Error Prone documentation for instructions on getting started with Error Prone and integration with your build system. The instructions below assume you are using Gradle; see the docs for discussion of other build systems. If you are building with JSpecify mode enabled, we recommend building with the most recent JDK available; see the wiki docs on JSpecify support for more details.

Gradle

Java (non-Android)

To integrate NullAway into your non-Android Java project, add the following to your build.gradle file:

gradle
plugins {
  // we assume you are already using the Java plugin
  id "net.ltgt.errorprone" version "<plugin version>"
}

dependencies {
  errorprone "com.uber.nullaway:nullaway:<NullAway version>"

  // Some source of nullability annotations; JSpecify recommended,
  // but others supported as well.
  api "org.jspecify:jspecify:1.0.0"

  errorprone "com.google.errorprone:error_prone_core:<Error Prone version>"
}

import net.ltgt.gradle.errorprone.CheckSeverity

tasks.withType(JavaCompile) {
  options.errorprone {
    check("NullAway", CheckSeverity.ERROR)
    option("NullAway:AnnotatedPackages", "com.uber")
  }
  // Include to disable NullAway on test code
  if (name.toLowerCase().contains("test")) {
    options.errorprone {
      disable("NullAway")
    }
  }
}

Let's walk through this script step by step. The plugins section pulls in the Gradle Error Prone plugin for Error Prone integration.

In dependencies, the first errorprone line loads NullAway, and the api line loads the JSpecify library which provides suitable nullability annotations, e.g., org.jspecify.annotations.Nullable. NullAway allows for any @Nullable annotation to be used, so, e.g., @Nullable from the AndroidX annotations Library or JetBrains annotations is also fine. The second errorprone line sets the version of Error Prone is used.

Finally, in the tasks.withType(JavaCompile) section, we pass some configuration options to NullAway. First check("NullAway", CheckSeverity.ERROR) sets NullAway issues to the error level (it's equivalent to the -Xep:NullAway:ERROR standard Error Prone argument); by default NullAway emits warnings. Then, option("NullAway:AnnotatedPackages", "com.uber") (equivalent to the -XepOpt:NullAway:AnnotatedPackages=com.uber standard Error Prone argument) tells NullAway that source code in packages under the com.uber namespace should be checked for null dereferences and proper usage of @Nullable annotations, and that class files in these packages should be assumed to have correct usage of @Nullable (see the docs for more detail). NullAway requires exactly one of the AnnotatedPackages or OnlyNullMarked configuration arguments to run, in order to distinguish between annotated and unannotated code. See the configuration docs for more details and other useful configuration options. For even simpler configuration of NullAway options, use the Gradle NullAway plugin. Finally, we show how to disable NullAway on test code, if desired.

We recommend addressing all the issues that Error Prone reports, particularly those reported as errors (rather than warnings). But, if you'd like to try out NullAway without running other Error Prone checks, you can use options.errorprone.disableAllChecks (equivalent to passing "-XepDisableAllChecks" to the compiler, before the NullAway-specific arguments).

Android

Versions 3.0.0 and later of the Gradle Error Prone Plugin no longer support Android. So if you're using a recent version of this plugin, you'll need to add some further configuration to run Error Prone and NullAway. Our sample app build.gradle file shows one way to do this, but your Android project may require tweaks. Alternately, 2.x versions of the Gradle Error Prone Plugin still support Android and may still work with your project.

Beyond that, compared to the Java configuration, the JSpecify dependency can be removed; you can use the androidx.annotation.Nullable annotation from the AndroidX annotation library instead.

Annotation Processors / Generated Code

Some annotation processors like Dagger and AutoValue generate code into the same package namespace as your own code. This can cause problems when setting NullAway to the ERROR level as suggested above, since errors in this generated code will block the build. Currently the best solution to this problem is to completely disable Error Prone on generated code, using the -XepExcludedPaths option added in Error Prone 2.1.3 (documented here, use options.errorprone.excludedPaths= in Gradle). To use, figure out which directory contains the generated code, and add that directory to the excluded path regex.

Note for Dagger users: Dagger versions older than 2.12 can have bad interactions with NullAway; see here. Please update to Dagger 2.12 to fix the problem.

JSpecify Mode / Guava

As of version 33.4.1, Guava uses JSpecify @NullMarked annotations for most of its packages, and hence NullAway will treat those packages as annotated by default. This treatment may lead to some false positives (due to handling of type variables), which can be mitigated by running NullAway in its (under-development) JSpecify mode. See the wiki docs on JSpecify support for more details.

Lombok

Unlike other annotation processors above, Lombok modifies the in-memory AST of the code it processes, which is the source of numerous incompatibilities with Error Prone and, consequently, NullAway.

We do not particularly recommend using NullAway with Lombok. However, NullAway encodes some knowledge of common Lombok annotations and we do try for best-effort compatibility. In particular, common usages like @lombok.Builder and @Data classes should be supported.

Since Lombok v1.18.34, Lombok adds @lombok.Generated by default to the methods/classes it generates. NullAway relies on this annotation to detect generated code within the in-memory Java AST and will ignore (i.e. not check) the implementation of this generated code, treating it as unannotated.
If you have explicitly disabled this behavior then NullAway will not correctly recognize Lombok-generated code, and compatibility may break.

If you are using a Lombok version earlier than 1.18.34, you must enable the following configuration option in an applicable lombok.config file:

properties
lombok.addLombokGeneratedAnnotation = true

Code Example

Let's see how NullAway works on a simple code example:

java
static void log(Object x) {
    System.out.println(x.toString());
}
static void foo() {
    log(null);
}

This code is buggy: when foo() is called, the subsequent call to log() will fail with an NPE. You can see this error in the NullAway sample app by running:

bash
cp sample/src/main/java/com/uber/mylib/MyClass.java.buggy sample/src/main/java/com/uber/mylib/MyClass.java
./gradlew build

By default, NullAway assumes every method parameter, return value, and field is non-null, i.e., it can never be assigned a null value. In the above code, the x parameter of log() is assumed to be non-null. So, NullAway reports the following error:

text
warning: [NullAway] passing @Nullable parameter 'null' where @NonNull is required
    log(null);
        ^

We can fix this error by allowing null to be passed to log(), with a @Nullable annotation:

java
static void log(@Nullable Object x) {
    System.out.println(x.toString());
}

With this annotation, NullAway points out the possible null dereference:

text
warning: [NullAway] dereferenced expression 'x' is @Nullable
    System.out.println(x.toString());
                        ^

We can fix this warning by adding a null check:

java
static void log(@Nullable Object x) {
    if (x != null) {
        System.out.println(x.toString());
    }
}

With this change, all the NullAway warnings are fixed.

For more details on NullAway's checks, error messages, and limitations, see our detailed guide.

Support

Please feel free to open a GitHub issue if you have any questions on how to use NullAway. Or, you can join the NullAway Discord server and ask us a question there.

Contributors

We'd love for you to contribute to NullAway! Please note that once
you create a pull request, you will be asked to sign our Uber Contributor License Agreement.

License

NullAway is licensed under the MIT license. See the LICENSE.txt file for more information.


Overview (`jdk-javac-plugin/README.md`)

Javac plugin for generating JSpecify JDK astubx file

This module and others contain logic to generate an astubx file from the
annotated JSpecify JDK. The generation works
in two stages:

  1. This module provides a javac plugin that gets injected into the build of the
    JSpecify JDK. It generates .json files capturing the nullability annotations
    in the JDK.
  2. We have a separate astubx generator (main entrypoint:
    com.uber.nullaway.jdkannotations.AstubxGeneratorCLI) that turns the .json
    files into an .astubx file.

To regenerate the file and run the main NullAway tests and self-check, run:

bash
./jdk-javac-plugin/regenerate-jspecify-jdk.sh

The script fetches a shallow checkout of the JSpecify JDK at the pinned
JDK_REVISION in the script. To reuse an existing clean checkout at that
revision, add --jdk-dir /path/to/jdk. You can pass --boot-jdk /path/to/boot-jdk
to give a path to the boot JDK to use to bootstrap the build;
without it, JDK configure searches for a compatible installed JDK.
Configuration, build output, and JSON files go into a fresh temporary directory.
Failed runs retain that directory and their logs; use --keep-temp to retain it
after successful runs as well. Use --verbose to stream command output while
also saving it to the logs. If all succeeds, the
nullaway/src/main/resources/jspecify-jdk.astubx file will be overwritten with
the new version.


Overview (`jar-infer/jar-infer-cli/README.md`)

NullAway jar-infer

Introduction

This extends NullAway to support Nullability inference on external libraries.

Requirements

  • Java 8
  • The Gradle build tool
  • The WALA analysis framework

Installation

Clone the repository, and then:

text
gradle wrapper
./gradlew build

This will pull in the required WALA jars and build the analysis code.

Usage

text
java -jar  -i  -o <out_path> [-p ] [-vdh]
 -i,--input-file      path to target jar/aar file
 -o,--output-file <out_path>   path to processed jar/aar file
 -p,--package        qualified package name
 -v,--verbose                  set verbosity
 -d,--debug                    print debug information
 -h,--help                     print usage information

Overview (`jdk-annotations/jdk-integration-test/README.md`)

These integration tests for the javac plugin work as follows. We have two versions of code used as a "library" for
input, in the sibling test-annotated and test-unannotated modules. For the test-annotated code, we generate an
astubx using the javac plugin (to generate JSON) and the astubx generator (to generate astubx from the JSON). Then,
when building the jar for the test-unannotated code, we include the generated astubx in the jar file. This module
depends only on the test-unannotated module, so the tests are run against the unannotated code, but with the astubx
available on the classpath. This allows us to test that the javac plugin is correctly generating the astubx and that
the astubx is correctly being loaded and used by NullAway.


Overview (`scripts/nullaway-suppression-remover/README.md`)

NullAway suppression remover

suppression-remover removes unnecessary checker entries from Java
@SuppressWarnings annotations. It temporarily removes every matching suppression,
compiles the target module, and restores the suppressions required to make the build
pass. The remaining source changes are the suppressions that the tool determined were
unnecessary. The technique is a heuristic, and is not guaranteed to remove all
unnecessary suppressions.

Although it was written for NullAway, the tool could be used with another Error
Prone checker whose diagnostics use the standard [CheckerName] format (this has
not been tested).

Prerequisites

  • Python 3.10 or newer
  • A working build for the project being processed
  • For the default build behavior, a Gradle project with either gradlew at its
    root or gradle on PATH

Install the command from this directory. Using a virtual environment keeps its
Python dependencies isolated:

bash
cd scripts/nullaway-suppression-remover
python3 -m venv /tmp/nullaway-suppression-remover-venv
source /tmp/nullaway-suppression-remover-venv/bin/activate
python -m pip install -e .

The suppression-remover command is then available whenever that virtual
environment is active.

Before running it

The tool edits Java source files in place and does not have a dry-run mode. Run it
from a clean Git worktree so that you can inspect or discard its changes. Also run
the relevant build once before using the tool; pre-existing build failures can
prevent it from determining which suppressions are needed.

Avoid interrupting the process while a build is running. The tool normally restores
files when it cannot produce a passing build, but an external interruption may leave
the temporary edits in place.

Usage

From anywhere inside the project to process, run:

text
suppression-remover MODULE CHECKER [OPTIONS]

MODULE is the module directory relative to the project root, and CHECKER is
the name printed in compiler diagnostics. For example, from the root of this

bash
suppression-remover nullaway NullAway --alt-suppression NullAway.Init

This scans every .java file below nullaway/src, treats both NullAway and
NullAway.Init as NullAway suppressions, and updates unnecessary annotations.

To process a whole project, including all of its submodules, use . for the module
and provide a build command that compiles everything:

bash
suppression-remover . NullAway --build-cmd "./gradlew build --continue"

In this mode, the tool scans for Java files beneath every directory named src
under the project root. It trusts that the supplied build command compiles all of
those sources and reports checker errors for them. This mode also works for a
single-module project. A custom build command is required when the module is ..

After the command succeeds, inspect the resulting changes and run the project's
normal test or verification command before committing them.

Options

--alt-suppression ALT
: Treat ALT as another suppression name for the same checker. The option can be
repeated, for example:

bash
suppression-remover app NullAway \
  --alt-suppression NullAway.Init \
  --alt-suppression NullAway.Optional

--project-root DIR
: Use DIR as the project root. Without this option, the tool walks upward from
the current directory and selects the first directory containing one of
settings.gradle, settings.gradle.kts, build.gradle, build.gradle.kts, or
pom.xml.

bash
suppression-remover subprojects/service NullAway --project-root /path/to/project

--build-cmd CMD
: Run CMD instead of the generated Gradle command. The command must compile all
source sets that should be checked, emit standard Java/Error Prone diagnostics,
and exit nonzero when checker errors occur.

This option is required for Maven and other non-Gradle builds, for processing a
whole project with MODULE set to ., and for unusual Gradle layouts:

bash
suppression-remover service NullAway \
  --build-cmd "mvn -pl service test-compile"

The value is split into arguments using shell-like quoting, but it is not run
through a shell. Shell operators such as &&, pipes, redirections, and environment
assignments are therefore not supported; put complex build logic in a script and
pass that script as the command instead.

--max-iterations N
: Set the maximum number of restore-and-verify iterations after the initial build.
The default is 5, and the value must be a positive integer. Increase it for
codebases where nested or interacting suppressions require more refinement passes,
or when removing suppressions across many modules:

bash
suppression-remover . NullAway \
  --build-cmd "./gradlew build --continue" \
  --max-iterations 10

--source-encoding ENCODING
: Read and write Java source files using ENCODING (default: utf-8). Use a
Python codec name such as iso-8859-1 for projects with legacy source files:

bash
suppression-remover app NullAway --source-encoding iso-8859-1

Use suppression-remover --help to display the command-line help.

Default Gradle build

For a specific non-root module, when --build-cmd is omitted, the tool discovers
source sets containing Java files under MODULE/src and runs their compile tasks
with --continue. For a module named service with main and test source sets,
the generated command is equivalent to:

bash
./gradlew :service:compileJava :service:compileTestJava --continue

A custom source set such as testFixtures is mapped to
compileTestFixturesJava.

What gets changed

Only @SuppressWarnings annotations containing an exact matching string are
considered. If the target checker is the only entry, the entire annotation is
removed. If an annotation also suppresses other checkers, it is rewritten with
those entries retained. For example:

java
@SuppressWarnings({"NullAway", "deprecation"})

becomes:

java
@SuppressWarnings("deprecation")

The tool parses Java syntax, so text in comments and string literals is ignored.
When it removes one value from a multiline annotation, it preserves the annotation's
multiline layout and indentation. It may normalize a single-line multi-checker
annotation that it updates. Trailing comments are removed.

Running the tool's tests

Install the test dependency and run pytest from this directory:

bash
python -m pip install -e '.[test]'
python -m pytest

--- METRICS ---