| name | nullability-control |
| description | Java null safety using NullAway and JSpecify annotations. Covers Gradle setup, package-info.java creation with @NullMarked, annotation patterns, and migration from legacy annotations using OpenRewrite.
|
| compatibility | Java 17+ projects using Gradle with Error Prone |
| metadata | {"version":"1.0.0","technology":"java","category":"static-analysis","tags":["java","nullability","nullaway","jspecify","static-analysis"]} |
Nullability Control
Compile-time null safety for Java using NullAway and JSpecify annotations.
When to use this skill
- Setting up null safety for new Java projects
- Migrating existing code to JSpecify annotations
- Fixing NullAway errors and warnings
- Creating package-info.java files with @NullMarked
- Understanding null safety annotation patterns
Skill Contents
Sections
Available Resources
📚 references/ - Detailed documentation
📦 assets/ - Templates and resources
Quick Start
1. Add Dependencies to Version Catalog
Add to gradle/libs.versions.toml:
[versions]
errorProne = "2.46.0"
nullaway = "0.13.1"
jspecify = "1.0.0"
[libraries]
error-prone-core = { module = "com.google.errorprone:error_prone_core", version.ref = "errorProne" }
nullaway = { module = "com.uber.nullaway:nullaway", version.ref = "nullaway" }
jspecify = { module = "org.jspecify:jspecify", version.ref = "jspecify" }
[plugins]
errorprone = { id = "net.ltgt.errorprone", version = "4.3.0" }
Note: Also register the plugin in settings.gradle under pluginManagement { plugins { ... } }
Versions verified: January 2026. Check for updates at NullAway releases and Error Prone releases.
2. Configure Error Prone + NullAway
In each module's build.gradle (not root project for multi-module):
plugins {
id 'java'
alias(libs.plugins.errorprone)
}
dependencies {
// NullAway + Error Prone
errorprone libs.error.prone.core
errorprone libs.nullaway
implementation libs.jspecify
}
import net.ltgt.gradle.errorprone.CheckSeverity
tasks.withType(JavaCompile).configureEach {
// Required for Error Prone 2.46.0+ on JDK 21 (OpenJDK only - not supported on Oracle JDK)
options.compilerArgs += ["-XDaddTypeAnnotationsToSymbol=true"]
options.errorprone {
// CRITICAL: Disable all Error Prone checks first to prevent build failures
// Error Prone enables many strict checks by default that fail on existing code
disableAllChecks = true
// Then enable only NullAway
check("NullAway", CheckSeverity.ERROR)
option("NullAway:OnlyNullMarked", "true")
}
}
Important Notes:
disableAllChecks = true is essential for existing projects. Error Prone enables many strict checks by default (StringCaseLocaleUsage, JavaTimeDefaultTimeZone, ReturnValueIgnored, etc.) that will fail most existing codebases.
- For multi-module projects, apply the Error Prone plugin in each subproject's
build.gradle, not via the root subprojects {} block. See multi-module-setup.md for details.
3. Create package-info.java for Each Package
Every package needs a package-info.java file:
@NullMarked
package com.example.mypackage;
import org.jspecify.annotations.NullMarked;
See package-info-templates.md for complete templates.
Key Principles
| Principle | Description |
|---|
| Non-null by default | In @NullMarked code, all types are non-null unless annotated @Nullable |
| Use JSpecify | Use org.jspecify.annotations.Nullable, not javax or JetBrains |
| Package-level marking | Apply @NullMarked to packages via package-info.java |
| OnlyNullMarked config | Use -XepOpt:NullAway:OnlyNullMarked=true (NullAway 0.12.3+) |
| Explicit nullability | Only annotate @Nullable; non-null is implicit |
| Type-use placement | Place @Nullable directly before the type (e.g., @Nullable String) |
JDK Compatibility Note: For full JSpecify support with generics, use JDK 22+ or JDK 21.0.8+ with -XDaddTypeAnnotationsToSymbol=true.
Gradual Adoption Strategy
With OnlyNullMarked mode, NullAway only checks code in @NullMarked packages. This enables gradual adoption:
- Add NullAway to build - Build passes immediately (no
@NullMarked code yet)
- Add
package-info.java to one package - Fix null-safety issues in that package
- Repeat for each package - Continue until all packages are covered
This approach allows incremental migration without blocking the build.
Package-info.java Pattern
Every package in your codebase should have a package-info.java:
@NullMarked
package com.bitso.myservice.domain;
import org.jspecify.annotations.NullMarked;
Why This Matters
- NullAway only checks code marked as
@NullMarked
- Without package-info.java, code is treated as unannotated
- Class-level @NullMarked is an alternative but package-level is preferred
Creating package-info.java Files
For each package directory:
find src/main/java -type d -exec sh -c '
if [ -n "$(ls -A "$1"/*.java 2>/dev/null)" ] && [ ! -f "$1/package-info.java" ]; then
echo "$1"
fi
' _ {} \;
Common Patterns
Nullable Parameters
public void process(@Nullable String input) {
if (input != null) {
System.out.println(input.length());
}
}
Nullable Return Values
public @Nullable User findUser(String id) {
return userMap.get(id);
}
Nullable Fields
public class Request {
private @Nullable String optionalField;
private String requiredField;
}
Collections with Nullable Elements
List<@Nullable String> items;
@Nullable List<String> maybeList;
References
Lombok Compatibility
Lombok works well with NullAway but requires configuration.
Required Configuration
Create lombok.config at project root:
lombok.addLombokGeneratedAnnotation = true
This makes NullAway skip Lombok-generated code (constructors, getters, setters, builders).
Lombok @NonNull Annotations
Keep Lombok @NonNull annotations - they serve a different purpose:
- Lombok's
@NonNull generates runtime null checks (throws NullPointerException)
- JSpecify's
@Nullable provides compile-time null safety (NullAway checks)
- These are complementary: Lombok for runtime protection, JSpecify for compile-time analysis
public record User(@NonNull String id, @NonNull String name) {}
Migration Strategy
| Annotation | Action |
|---|
lombok.NonNull | Keep - provides runtime null checks |
javax.annotation.Nonnull | Remove - redundant in @NullMarked code |
javax.annotation.Nullable | Replace with org.jspecify.annotations.Nullable |
org.jetbrains.annotations.NotNull | Remove - redundant in @NullMarked code |
org.jetbrains.annotations.Nullable | Replace with org.jspecify.annotations.Nullable |
Validation Results
This skill has been validated against multiple real-world multi-module Spring Boot projects with various configurations including protobuf, gRPC, JOOQ, and Lombok.
Key Findings
disableAllChecks = true is essential - Error Prone has many strict checks enabled by default that fail most existing code
- JDK 21 requires
-XDaddTypeAnnotationsToSymbol=true - Error Prone 2.46.0+ fails without this flag on JDK 21
- Apply plugin per-subproject - The Error Prone plugin must be in each module's
plugins {} block, not via root subprojects {}
- Register plugin in settings.gradle - Add
id 'net.ltgt.errorprone' version '4.1.0' in pluginManagement { plugins { } }
- Lombok compatibility - Works well; add
lombok.config with lombok.addLombokGeneratedAnnotation = true
- Keep Lombok
@NonNull - These provide runtime null checks; only remove javax.annotation.Nonnull and similar
- Legacy annotation migration - Remove
@Nonnull from legacy packages (javax, JetBrains) since non-null is default in @NullMarked code
- OnlyNullMarked mode - Enables gradual adoption; build passes immediately with no @NullMarked code
Related Skills