hiero-ledger / hiero-ledger/hiero-sdk-java
Replace JSR-305 (com.google.code.findbugs:jsr305) with JSpecify
- Dominant language
- Java
- Stars
- 264
- Forks
- 192
- Avg merge
- 2d
- Merged PRs (30d)
- 39
Description
The SDK's nullability contract is expressed with JSR-305 / FindBugs annotations:
| Annotation | Occurrences | Where |
|---|---|---|
| `@Nullable` | 793 | `sdk/src/main` (150 files) |
| `@Nonnegative` | 97 | `sdk/src/main` |
| `@Nonnull` | 2 | `sdk/src/main/java/com/hedera/hashgraph/sdk/PendingAirdropId.java:45,54` |
| `@ParametersAreNonnullByDefault` | 1 | `sdk/src/main/java/com/hedera/hashgraph/sdk/package-info.java` |
| `@Nullable` / `@Nonnegative` | 51 | `tck/src` (5 files) |
| `@Nullable` | — | `sdk/src/test` (8 files), `examples` (1 file) |
The dependency is declared in `hiero-dependency-versions/build.gradle.kts:17`:
```kotlin
api("com.google.code.findbugs:jsr305:3.0.2") { because("java.annotation") }
```
and consumed as `requires static java.annotation` in `sdk/src/main/java/module-info.java:15`,
`sdk-full/src/main/java/module-info.java:15`, `sdk/src/testIntegration/java/module-info.java:8`,
and as `requiresStatic("java.annotation")` in `tck/build.gradle.kts:26`.
## Why this should change
1. **JSR-305 is dead.** `com.google.code.findbugs:jsr305:3.0.2` was released in 2017. The underlying JSR was withdrawn and never standardised; there is no maintainer, no CVE response, and no path to fixes. It is a permanent, unmaintained dependency in the SDK's published POM.
2. **The module name is a lie.** The artifact is mapped to the JPMS module name `java.annotation`, which is actually the module name of `javax.annotation-api` (JSR-250: `@Resource`, `@PostConstruct`, `@Generated`) — a completely different API. On top of that, the `java.*` namespace is reserved for JDK platform modules; claiming it for a third-party jar is a workaround, not a design. JSpecify ships a real `module-info` with the honest name `org.jspecify`.
3. **Split-package / ecosystem collision.** `javax.annotation` is shared between JSR-305 and JSR-250. Any consumer that also has `javax.annotation-api` on the path hits a split package on the module path. This already shows up inside this repo: `tck/.../response/TopicInfoResponse.java:4` and `tck/.../response/schedule/ScheduleInfoResponse.java:4` import `jakarta.annotation.Nullable` (transitively from Spring Boot) while five sibling classes in the same package tree import `javax.annotation.Nullable`. Two different `@Nullable` types in one response model is exactly the confusion this dependency causes.
4. **JSpecify is what tooling actually targets now.** It is a joint specification backed by Google, JetBrains, Oracle, Microsoft, Uber and others, with a stable 1.0 release and precisely defined semantics (unlike JSR-305, whose meaning each tool guessed at). Kotlin, IntelliJ, Error Prone / NullAway and the Eclipse null analysis all understand it; Kotlin honours JSpecify in strict mode by default from 2.1 onwards. For our Kotlin and Android users that means the SDK's nullability contract is actually enforced at their compile time instead of degrading to platform types.
5. **Better defaults.** `@ParametersAreNonnullByDefault` only covers parameters. JSpecify's `@NullMarked` covers parameters, return types, fields and type arguments, so the contract stops being half-specified.
## Proposed change
Migrate the whole repository to `org.jspecify:jspecify` and drop the JSR-305 dependency completely.
### Build
- Add to `hiero-dependency-versions/build.gradle.kts`:
```kotlin
api("org.jspecify:jspecify:1.0.0") { because("org.jspecify") }
```
- Remove the `com.google.code.findbugs:jsr305` constraint.
- `requires static java.annotation` → `requires static org.jspecify` in `sdk`, `sdk-full` and `sdk` testIntegration `module-info.java`.
- `requiresStatic("java.annotation")` → `requiresStatic("org.jspecify")` in `tck/build.gradle.kts`.
- Drop the `jsr305` constraint from `examples/build.gradle.kts` if it is no longer needed there.
### Source
- `javax.annotation.Nullable` → `org.jspecify.annotations.Nullable` (mechanical, but see "TYPE_USE" below).
- `sdk/.../sdk/package-info.java`: `@ParametersAreNonnullByDefault` → `@NullMarked`.
- The two `@Nonnull` usages in `PendingAirdropId` become redundant under `@NullMarked` and can be deleted.
- Align the two `jakarta.annotation.Nullable` imports in `tck` with the rest.
- Apply the same swap in `sdk/src/test`, `tck` and `examples`.
### Open decision: `@Nonnegative`
JSpecify deliberately covers **only** nullness — there is no `@Nonnegative` equivalent, and there will not be one. The 97 `@Nonnegative` usages (plus 2 in `tck`) need an explicit decision, e.g.:
- **(a)** Drop the annotation and state the constraint in Javadoc. Simple, honest — `@Nonnegative` was never enforced by anything in our build anyway.
- **(b)** Drop the annotation and add real precondition checks (`if (x < 0) throw new IllegalArgumentException(...)`) where a negative value is actually a bug. Stronger guarantee, but a behaviour change that needs its own review.
- **(c)** Keep JSR-305 solely for `@Nonnegative`. Not recommended — it defeats the purpose of the issue.
Recommendation: (a) for this issue, with (b) tracked separately if wanted.
## Risks / things to verify
- **TYPE_USE placement.** JSpecify annotations are `@Target(TYPE_USE)` only. For most of our 793 sites the textual position is unchanged, but they differ for arrays, nested/qualified types and generic type arguments (`@Nullable String[]` vs `String @Nullable []`). The migration must not be a blind `sed`; the compiler will reject truly wrong placements, but semantically-shifted ones (e.g. on generics) need review.
- **`@NullMarked` is broader than `@ParametersAreNonnullByDefault`.** Turning it on asserts that every unannotated return type and field in the package is non-null. That is very likely *not* true everywhere today. Expect this step to surface genuinely missing `@Nullable` annotations on return types — which is a bug-finding win, but means the migration is not purely mechanical.
- **Retention change.** JSR-305's `@Nullable` has `RUNTIME` retention; JSpecify's has `CLASS`. Anything reading these annotations reflectively at runtime would stop seeing them. We are not aware of such a consumer, but it should be confirmed before release and mentioned in the changelog.
- **Kotlin consumers.** This is the point of the change, but it is also the compatibility risk: Kotlin users currently see platform types for unannotated returns and will see strict types afterwards, so previously-compiling Kotlin code can start failing. `example-android` (Kotlin) is a useful in-repo canary. This belongs in the release notes.
- **No API-compatibility tooling exists in this repo** (no japicmp/revapi), so the public-signature effect has to be reviewed manually.
- **Android.** JSpecify is a single small jar with no transitive dependencies and is Android-friendly; `example-android` should still be built as part of the verification.
## Acceptance criteria
- [ ] `com.google.code.findbugs:jsr305` no longer appears in any module's compile or runtime classpath, nor in the published POMs (`./gradlew dependencies` / generated POM check).
- [ ] No `javax.annotation.*` or `jakarta.annotation.*` imports remain in `sdk`, `sdk-full`, `tck`, `examples`.
- [ ] `module-info.java` of `sdk` and `sdk-full` declares `requires static org.jspecify`; the bogus `java.annotation` module name is gone from the repo.
- [ ] `@NullMarked` is applied at package level and the build is clean, with any newly-surfaced nullable return types annotated rather than suppressed.
- [ ] A decision on `@Nonnegative` is implemented and documented.
- [ ] `examples`, `tck` and `example-android` build and run against the changed SDK.
- [ ] The nullability change and its Kotlin impact are noted in `CHANGELOG.md` for the next release.
## References
- JSpecify: https://jspecify.dev — `org.jspecify:jspecify:1.0.0`
- `hiero-dependency-versions/build.gradle.kts:17` — the current `jsr305` → `java.annotation` mapping
- `sdk/src/main/java/com/hedera/hashgraph/sdk/package-info.java` — current `@ParametersAreNonnullByDefault`
- `tck/.../TopicInfoResponse.java:4`, `tck/.../schedule/ScheduleInfoResponse.java:4` — the existing `jakarta` vs `javax` inconsistency
Contributor guide
Assessment
This issue has not been assessed yet.