2025008. Adoption of JSpecify for Null-Safety

  • Status: Accepted – Partially implemented accross multiple PRs

  • Deciders: Hiroshi Miura, Jean-Christophe Helary, OmegaT Development Team

  • Date: 2025-11-18

  • Updated: 2026-08-21

Context and Problem Statement

NullPointerExceptions (NPEs) are a common source of runtime errors in Java applications. While static code analysis tools (like SpotBugs and NullAway) can detect potential nullability issues, they rely on accurate annotations to function effectively.

Historically, the Java ecosystem has suffered from a fragmentation of nullability annotations (javax.annotation, com.intellij.annotations, android.support.annotation, etc.), leading to inconsistent analysis and library interoperability issues.

In OmegaT:

  1. We have previously relied on transitive dependencies (e.g., via language-tool) or IDE-specific annotations (IntelliJ) for null checks.

  2. Discussions in December 2022 identified the need for a standard approach.

  3. In March 2025, an RFC was raised to actively use nullity annotations to reduce the 860+ warnings/errors detected by SpotBugs.

  4. Modern Java development practices (influenced by Kotlin and other null-safe languages) advocate for explicit nullability definitions.

We need a standardized, robust, and modern way to declare nullability intent in the codebase to improve quality assurance and developer productivity.

Decision Drivers

  • Standardization: The need to move away from proprietary or deprecated annotation libraries (like JSR-305).

  • Code Quality: Reducing the risk of NPEs through compile-time checks and static analysis.

  • Tooling Support: Ensuring compatibility with modern static analysis tools like NullAway and SpotBugs.

  • Clarity: Making APIs self-documenting regarding whether they accept or return null.

Considered Options

  • Option 1: Continue using ad-hoc annotations (IntelliJ/FindBugs).

    • Pros: No immediate changes required.

    • Cons: Fragmentation, lack of standard enforcement, dependency on specific IDEs or older libraries.

  • Option 2: Adopt JSpecify (org.jspecify.annotations).

    • Pros: Industry standard for JVM nullability, supported by major industry players (Google, JetBrains, etc.), provides @NullMarked for comprehensive defaults.

    • Cons: Requires adding a dependency (though lightweight), requires migration effort.

Decision

We will adopt JSpecify as the standard library for nullability annotations in the OmegaT project.

  1. Dependency: We add org.jspecify:jspecify as a compile-time dependency.

  2. Default Non-Null Strategy: We will utilize the @NullMarked annotation at the package level (via package-info.java) or class level. This inverts the default behavior: everything is assumed non-null unless specified otherwise.

  3. Explicit Nullable: We will use @Nullable explicitly for any field, parameter, or return value that can legitimately be null.

  4. Migration: Existing annotations (from IntelliJ or other sources) will be replaced with JSpecify equivalents progressively.

This decision aligns with ADR 2025003 Comprehensive Static Code Analysis Strategy.

Consequences

Positive

  • Reduced Bugs: Static analysis tools can now rigorously enforce null safety, catching bugs before runtime.

  • Clearer API Contracts: Developers immediately know if a method returns null or if an argument requires a non-null value.

  • Reduced Boilerplate: Using @NullMarked reduces the visual noise of having to annotate every single non-null parameter with @NonNull.

  • Modernization: Aligns OmegaT source code with current Java ecosystem standards.

Negative

  • Initial Noise: Enabling stricter checks may result in a temporary spike in linter warnings that need to be resolved or suppressed.

  • Learning Curve: Developers must strictly adhere to annotating nullable types, otherwise, the “default non-null” assumption may hide issues (though tools usually catch inconsistencies).

  • Short‑term Workload Increase: The project will experience a sustained period of warning cleanup and annotation work across modules (e.g., MissingOverride, Javadoc tag hygiene, nullability contracts). This adds overhead until the codebase reaches a new steady state with low warning counts.

  • CI Signal‑to‑Noise Risk: The elevated number of warnings can obscure genuinely critical findings unless baselines and thresholds are tuned.

Note: The observed messages reported as “errors” are warnings emitted by Error Prone/NullAway and related linters; the build remains successful. The heightened visibility is intentional, following the introduction of Error Prone to the core (PR #1496) and later expansion to modules (PR #1692), and supports the migration to JSpecify.

Measuring @NullMarked Coverage

The repository includes tool/nullmarked_coverage.py, a lightweight Python utility for tracking the progress of JSpecify adoption. It reports how many top-level Java types are covered by @NullMarked, either through package-level defaults in package-info.java or by class-level annotations.

The tool classifies each top-level type as one of:

  • package-marked: the type belongs to a package whose package-info.java carries @NullMarked.

  • class-marked: the type declaration itself carries @NullMarked.

  • unmarked: neither the package nor the type is marked.

  • Types or packages annotated with @NullUnmarked are treated as unmarked.

Typical usage from the repository root:

python3 tool/nullmarked_coverage.py .
python3 tool/nullmarked_coverage.py . --source-set main
python3 tool/nullmarked_coverage.py . --by-package --source-set main
python3 tool/nullmarked_coverage.py . --list-unmarked --source-set main
python3 tool/nullmarked_coverage.py . --csv nullmarked-coverage.csv

Useful options include:

  • --source-set SOURCE_SET: restricts the report to a source set such as main, test, or testAcceptance; the option can be repeated.

  • --by-package: reports coverage per package instead of per module.

  • --min-types N: hides rows with fewer than N top-level types.

  • --list-unmarked: lists unmarked files, largest first, which is useful for selecting migration targets.

  • --csv FILE: writes per-type records to a CSV file for further analysis.

The default report groups results by Gradle module and source set. It shows the total number of top-level types, how many are covered by @NullMarked, how many remain unmarked, the coverage percentage, total lines of code, and lines of code still unmarked.

This tool is intended as a migration aid rather than a strict quality gate. It helps developers identify remaining unmarked areas, compare progress between pull requests, and prioritize packages or modules where adding package-info.java with @NullMarked would have the largest effect.

Implementation Progress

The adoption of JSpecify is being carried out incrementally across the codebase through focused pull requests:

References