PROPOSAL: Adopt jSpecify nullability annotations
envgap__oshi__oshi-3593
01 / FAILURE SIGNATURE
As reported upstream
Adoption is what moved me: **Spring Framework 7 / Spring Boot 4 went GA in November 2025 with jSpecify as the portfolio-wide null-safety standard**, deprecating Spring's own `@Nullable` / `@NonNullApi` / `@NonNullFields` in favor of it. The jspecify.dev participant list covers Google, JetBrains, Oracle, Microsoft, Uber and others, and NullAway/Error Prone consume it directly. Separately, Uber's NullAway — the main enforcement tool — has a [proposal in front of the Apache Incubator](https://cwiki.apache.org/confluence/display/INCUBATOR/Proposals) (created 2026-07-16), which cites growing jSpecify adoption as the motivation. This looks like the consolidation we were waiting for in 2020.
Not a benchmark task.
- The project already builds and runs before the fix, so there is nothing to repair.
02 / ENVIRONMENT RECIPE
- Base commit
84c737cfa911ace6c24525013158b078bf5fd61c- Manifest
pom.xml- Reproduce
Awaiting issue-specific recipe- Run under trace
Awaiting a meaningful runtime command
03 / ORIGINAL ISSUE TEXT
oshi/oshi #3593 · read the original issue
## Summary I'd like to discuss adding [jSpecify](https://jspecify.dev/) (`org.jspecify:jspecify`) as an optional, compile-time-only dependency, and annotating nullability across OSHI — starting with the public API and extending inward over time. **This is a proposal for discussion, not a PR.** I'd like to leave it open at least a week. I'm specifically re-opening a decision we made in 2020, so I want to lay out what has and hasn't changed. ## We said no to this in 2020 In #1160 I added `@ThreadSafe` / `@Immutable` / `@NotThreadSafe` from JSR-305, then backed them out in #1161 and defined our own instead. Two reasons: 1. **JPMS.** `javax.annotation` was split across multiple artifacts, which Java 9+ modules reject. 2. **Licensing.** The `javax.*` packaging ran afoul of the Oracle Binary Code License. @hazendaz — you gave the decisive advice there, from the SpotBugs side: > I would not use JSR305 right now or any of the numerous duplicate solutions. After 'jakarta' is officially > released in June and sometime after when all containers fully support it, the JSR305 is likely to be > consumed into jakarta annotations. I agreed and wrote: > I can probably just define those within OSHI (to preserve their presence in the javadocs) and hold off on > any more expansive use of the null-related, or check-return, or similar until JSR305 is in Jakarta. Or, > better, since I'm looking at an API switch, perhaps just start making use of `Optional`. Six years on, neither of those happened. Jakarta's common-annotations never took on the nullability annotations. And we never adopted `Optional` in the API — there are currently zero uses of it in `oshi.hardware` or `oshi.software.os`. Our own annotations still carry the javadoc line *"a temporary workaround until it is available in `jakarta.annotations`"*, which is now stale. So the "wait" was correct, but the thing we were waiting for arrived under a different name. ## What changed: jSpecify jSpecify is the effort that actually shipped, and it clears both 2020 objections: | 2020 objection | jSpecify 1.0.1 | |---|---| | `javax.annotation` split package breaks JPMS | `org.jspecify.annotations`, single artifact, real `module-info` (module `org.jspecify`) | | Oracle BCL covers `javax.*` | Apache 2.0 | It is deliberately tiny — **four annotations, nothing else**: | Annotation | Target | Purpose | |---|---|---| | `@Nullable` | `TYPE_USE` | this type usage may be null | | `@NonNull` | `TYPE_USE` | this type usage is not null (rarely needed in a marked scope) | | `@NullMarked` | `MODULE, PACKAGE, TYPE, METHOD, CONSTRUCTOR` | in this scope, unannotated types are non-null by default | | `@NullUnmarked` | `PACKAGE, TYPE, METHOD, CONSTRUCTOR` | undo the above | No `@CheckReturnValue`, no `@ThreadSafe`, no concurrency vocabulary — so **this does not replace or overlap our `oshi.annotation.concurrent` package**. It's purely the nullability slice. Artifact facts, all verified against the published jar rather than the docs: - **3,064 bytes. Zero transitive dependencies.** Apache 2.0. - Annotation classes are **major 52 — Java 8**, so `oshi-common` and `oshi-core` can use them. - Valid OSGi bundle as published: `Bundle-SymbolicName: org.jspecify.jspecify`, `Export-Package: org.jspecify.annotations;version="1.0.1"`. - Latest is **1.0.1** (2026-07-29); 1.0.0 was a multi-release jar, 1.0.1 moved `module-info.class` to the jar root to stop breaking Android consumers. Adoption is what moved me: **Spring Framework 7 / Spring Boot 4 went GA in November 2025 with jSpecify as the portfolio-wide null-safety standard**, deprecating Spring's own `@Nullable` / `@NonNullApi` / `@NonNullFields` in favor of it. The jspecify.dev participant list covers Google, JetBrains, Oracle, Microsoft, Uber and others, and NullAway/Error Prone consume it directly. Separately, Uber's NullAway — the main enforcement tool — has a [proposal in front of the Apache Incubator](https://cwiki.apache.org/confluence/display/INCUBATOR/Proposals) (created 2026-07-16), which cites growing jSpecify adoption as the motivation. This looks like the consolidation we were waiting for in 2020. ## How OSHI actually uses null today I measured this before proposing, because it determines whether the change is worth it. **The public API is already almost entirely null-free.** Across the 39 files in `oshi.hardware` and `oshi.software.os` — roughly 660 public methods and 403 `@return` tags — only about **7 methods document a nullable return**: - `OperatingSystem.getProcess(int)` - `OSVersionInfo.getVersion()` / `.getCodeName()` / `.getBuildNumber()` - `PowerSource.getManufactureDate()` - `InternetProtocolStats.IPConnection.getState()` …and about **6 nullable parameters**, mostly the optional `filter` / `sort` arguments on the `getProcesses` family, plus `ApplicationInfo`'s `additionalInfo` map and a couple of constructor arguments. Instead of null, we return sentinels, consistently: - **Strings** → `Constants.UNKNOWN` or `""` (~336 references) - **Collections** → `Collections.empty*`, never null (~162) - **Numerics** → `0`, `-1`, or `NaN` `Sensors`' class javadoc states the policy outright: *"Users should expect, test for, and handle zero values and/or empty arrays."* Worth noting: **this convention is not written down as policy anywhere** — not in FAQ.md, UPGRADING.md, or CONTRIBUTING.md. It's re-stated per-method in javadoc prose and otherwise carried by convention. Part of the appeal here is turning that prose into something tools can read. **Below the API line, the picture inverts.** There are roughly **275 `return null` statements** in main sources (oshi-common 82, oshi-core 75, oshi-core-ffm 118). Some of that is genuinely meaningful — we have eight `java:S1168` suppressions where null and empty are deliberately different answers, e.g. in `MacGpuStats`: > Returning `null` rather than an empty map when the card's statistics cannot be read at all is > load-bearing. That three-state distinction (read-and-empty vs. could-not-read) is exactly the kind of thing `@Nullable` documents well, and exactly the kind of thing a careless "just return empty" cleanup would destroy. ## What I'd propose Phased, one reviewable PR at a time. Note that `@NullMarked` on a `package-info.java` explicitly **does not apply to subpackages**, so each package is an explicit opt-in — which makes the phasing natural rather than arbitrary. - **Phase 1** — `@NullMarked` on `oshi.hardware` and `oshi.software.os`, plus the ~13 explicit marks listed above. Small, self-contained, and it's the part that benefits downstream users. - **Phase 2+** — extend package by package into `oshi.util`, `oshi.driver`, and the platform implementations, where the 275 null returns live. Dependency would be declared `<optional>true</optional>` (mirroring how we already treat `jLibreHardwareMonitor`), so it stays off consumers' transitive classpath. For OSGi, bnd would get `org.jspecify.annotations;resolution:=optional`. This is build-time and static-analysis metadata. Annotations are inert at runtime — no behavior change, no performance cost, no API change for anyone consuming OSHI. ## Open question **Tool support is still maturing.** Sonar is a jSpecify participant, but their Java analyzer has gaps today — `@NullMarked` isn't always honored, and there are open false-positive reports against jSpecify-annotated code. My experience reporting false positives to Sonar has shown a supported path to getting them addressed, and adopting the standard is what puts us in a position to file them. Near-term the concrete benefit is javadoc and IDE support, with analyzer coverage improving on its own schedule. ## Alternatives - **Do nothing.** The javadoc prose already documents the handful of nullable returns, and the API is ~98% null-free by convention. This is a real option. - **Define our own `@Nullable`**, as we did for the concurrency annotations. Cheapest, no dependency — but it buys nothing from tooling, which is the entire point. - **Wait longer** for Sonar's support to mature before annotating anything. Interested in thoughts — particularly on whether maturing tool support is a reason to wait or a reason to get in early. And @hazendaz, given you steered the 2020 call, I'd especially value your read on whether jSpecify is genuinely different this time or just the next entry in the "numerous duplicate solutions" list.
04 / LABELS
Labels from the report text only; not yet run
No supported category has been assigned.
Label rules and the text that matched
[]