Null Safety using JSpecify

Hey all,

I’ve been looking into JSpecify. It looks interesting. Especially for improving null safety and making our APIs clearer.

But I’m unsure if it’s worth the effort for a project of our size. Some parts of our codebase are really large, and retrofitting everything might be painful. On the other hand, maybe it could make sense for new modules…

Has anyone here tried using JSpecify in a real project?

Any pros/cons you’ve seen in practice?

Would be cool to hear what others think before we dive in.

Null safety is important and it has been always a pain to me that the JSR-305 did not make it to a real standard. I would love to see guarantees on nullability of parameters and return values.

Performing this throughout the code base is an enormous effort. I would start with the “public” API, thus nothing from “impl” packages. Or where it is really obvious what the guarantees are.

Deciding on a framework / standard is another thing. I never heard of JSpecify so far, so thank you for pointing to it. Its release is already 1 year old. Is it sta(b)le? Not sure.

This would be something for an ADR, if we want to proceed further. Once introduced, this would imply changes to a lot of code. And bears also risks, if guarantees (esp. non-null) are not held by callers.

Btw. in my current customer project we simply use Objects#requireNonNull to check parameters. However, there is no way to document that a method guarantees it does not return null.

What do others think?

I have seen the static code analysis of IntelliJ working quite well with different annotations, and a lot of people in the Java community said very positive things about JSpecify.

I’m hesitating a little bit because in my carreer I have seen plenty “Nullability Annotation” libraries come and go, and as @kthoms correctly mentioned, JSpecify is not that old. On the other hand, Spring 7 will add JSpecify to its whole API, so I believe that it is here to stay, now: Null-safety :: Spring Framework

Wondering if Spring’s decision to adopt JSpecify will push adoption more broadly. Especially on the user side. It might also put some “pressure” on projects like Operaton that provide Spring integration, since users likely won’t want to deal with more than one nullability framework.

I agree that this is something we should capture in an ADR to guide our decision.

Still, any feedback or experiences are very welcome to give us a better understanding of the topic before we move ahead with the ADR. Don’t want to close the thread just because we’re deciding to write one.

Just realized that at least in the engine module, null safety is already being checked using the EnsureUtil class. Maybe we should stick with that instead of using Objects.requireNonNull directly?

Options to discuss in the ADR would be:

  • stick with EnsureUtil / Objects.requireNonNull (status quo)
  • use JSpecify only for new public APIs
  • use JSpecify for all public APIs (but not internals)
  • adopt JSpecify project-wide (incl. internal code)
  • defer adoption and revisit later
  • use another annotation framework (e.g. JetBrains)

TLDR;

  • adopt JSpecify project-wide (incl. internal code)
  • use Objects.requireNonNull additionally on public API arguments
  • deprecate EnsureUtil and its logger and refactor to Objects.requireNonNull

Details:

The annotations by JSpecify (or others) help documenting the intent of nullability, and IDEs or Static Analysis Tools help to detect issues when this contract broken. However, they do not enforce at runtime when called at runtime that clients using public API follow this contract. Therefore a runtime check on public API seems mandatory to me.
After reading a bit more on JSpecify, and with the knowledge that Spring is adopting it, I would be in favor of using it, both on public and internal API. However, on public API I would additionally enforce mandatory non-null arguments by Objects.requireNonNull.
EnsureUtil has been introduced in 2014, before Objects.requireNonNull existed (Java 9). That method is also more performant, because it is inlined by @ForceInline. Thus I would actually propose an issue to deprecate EnsureUtil and refactor usages.