Java News

One Could Simply Add Nullability Check Support… Without Even Noticing It

(The Growing Pains of a Good Idea: What Happens After JSpecify Adoption Begins)

The good news: JSpecify is finally here

JSpecify adoption is accelerating. You don’t even need to “install” it – nullability support now arrives automatically with your dependencies. 

If you somehow missed the long history behind the JSpecify effort and want a quick refresher on what JSpecify actually is and why it matters, don’t worry you’ll find links to great introductory talks and resources at the end of this post. They provide background, practical examples, and a clear explanation of the goals behind the specification.

Frameworks and libraries like Spring Boot 4, Spring Framework 7 a lot of related libraries in Spring family, JUnit 6, and Guava 33.4+ already ship with JSpecify annotations. And this is just the beginning – more are on the way. 

This means that, as soon as you upgrade, IntelliJ IDEA starts analyzing your code using JSpecify semantics. The IDE now gives you smarter data-flow analysis, full generic support, and precise nullability checks, often revealing hidden issues you didn’t know were there.

In the upcoming IntelliJ IDEA 2025.3, JSpecify becomes the preferred nullability source. When it’s present on the classpath, the IDE automatically recognizes and even generates JSpecify annotations via quick-fixes and refactorings.

It’s a big win for null safety in Java – but it hasn’t removed all the difficulty from development.

The hard part: Migrating your application code

Once your dependencies are annotated, your own code becomes the next bottleneck.

In theory, migration should be simple: use OpenRewrite recipes or else swap @Nullable and @NonNull with their JSpecify equivalents via the Structural Search and Replace dialog.

In practice, however, it’s rarely that smooth.

JSpecify introduces a typed annotation model that older frameworks didn’t use. Type-use annotations apply not only to declarations, but to type arguments and generics, which changes how nullability flows through your program.

If you try to just add @NullMarked at the package level, suddenly all unannotated references in that package become non-null, and IDEA starts throwing warnings about every possible inconsistency.

As a result, the migration process ends up requiring you to re-annotate everything.

Alternatively, you can limit the scope, but then you’ll have to live with mixed semantics: either you’ll need to learn to tolerate all the warnings or disable analysis entirely.

With static analyzers like NullAway, you can restrict checks to specific packages, but that’s really only a half-measure – and the results will still differ from what you see in IDEA.

What we learned from testing migrations

At JetBrains, we encountered the same problems while testing JSpecify migration on sample Spring applications and the 2025.3 EAPs of our IDEs.

We also received detailed feedback from the Spring team and early adopters who tried migrating to JSpecify before this latest release.

Two main issues surfaced:

  1. IntelliJ IDEA still reported warnings even when a project passed a NullAway build cleanly.

Sometimes this means that IDEA found a real problem that NullAway’s coverage couldn’t detect. However, that wasn’t the case in this example:

The warnings that IntelliJ IDEA threw were technically correct as per the JSpecify specification because the ResponseEntity class is annotated with @NullMarked on the package level. This was confirmed by tests and upon review by the specification’s authors, but this information is not of significant value.

Unexpectedly, a seemingly improved implementation that considers generic types caused more issues for users, generating numerous warnings that surfaced immediately after migrating to the library’s new version.

  1. When users have to use suppressions in their framework-specific analysis, all of the ones they use need to be duplicated for every static analysis tool because there’s no standard for suppression constants.

This mismatch between the IDE and CI meant that developers couldn’t get a single, reliable picture of nullability. And if achieving that alignment required significant manual work, then JSpecify risked becoming a library-only standard, not one for application code.

Just a couple of weeks ago, this would’ve been the end of our blog post. We’d have just had to say that real life is hard, and “One does not simply integrate nullability to codebase”, even with fifteenth standard.

picture with Boromir
Picture with Boromir

But the lessons we’ve learned show us that something valuable can only be achieved when vendors and users work together.

The outcome: Alignment in IntelliJ IDEA 2025.3

Starting with IntelliJ IDEA 2025.3, JSpecify won’t just be supported – its user experience will be aligned with other analysis tools, including NullAway.

This includes:

  • Improved consistency of JSpecify inspection results with the specification.
  • Better matching of warnings between the IDE and build tools.
  • Smoother handling of mixed-annotation code during migration.

Our goal is to ensure that when your build passes with NullAway, your IDE view matches that reality – so you can rely on a single source of truth for nullability.

Regarding the first issue of excessive warnings, we opened IDEA-381329 following discussions with the community and feedback from the Spring applications migration tests. The eventual solution was discovered by Chris Povirk, who left the following comment on the issue:

And with some small adjustments to the inspection settings, the massive number of warnings disappear just like that.

The same goes for the second challenge – the lack of standard suppression constants for framework-specific analysis. Even with aligned semantics, static analysis tools interpret suppressions differently.

The JSpecify specification deliberately doesn’t standardize them, leaving each tool to define its own conventions.

  • IntelliJ IDEA uses standard inspection IDs such as @SuppressWarnings(“NotNullFieldNotInitialized“).
  • NullAway uses dedicated constants like @SuppressWarnings(“NullAway“) and more specific ones such as @SuppressWarnings(“NullAway.Init“) for initialization-related issues.

According to the Spring Framework 7.0 documentation, developers should use @SuppressWarnings(“NullAway.Init“) when framework-controlled initialization confuses static analysis:

@Component
public class OrderService implements InitializingBean {

    private Repository repo;

    @Override
    public void afterPropertiesSet() {
        // initialization logic happens here
    }

    public void process(Order order) {
        repo.save(order);  // ⚠️ NullAway may report "field repo not initialized"
    }
}

Even though the Spring container guarantees initialization, NullAway doesn’t see lifecycle callbacks and may flag the field.

The official recommendation is:

@SuppressWarnings(“NullAway.Init“)

private Repository repo;

IntelliJ IDEA, on the other hand, is Spring-aware and correctly recognises @Autowired, @Inject, and lifecycle interfaces, so it usually doesn’t warn about these patterns at all.

To prevent divergence, the IDEA and NullAway teams coordinated their efforts:

  • IntelliJ now recognises NullAway suppression constants such as NullAway.Init.
  • NullAway, starting from recent builds, accepts IntelliJ-style suppression IDs (e.g. NotNullFieldNotInitialized) for compatibility.
  • The ongoing collaboration is tracked in IDEA-376483.

Together, these changes make suppressions portable and make nullability checks consistent between your IDE and CI builds.

The work continues

Even though JSpecify 1.0.0 has been released and widely adopted, work on the specification and tooling continues.

Discussions are ongoing about:

  • Unified suppression identifiers that all tools can recognise.
  • New annotations for finer nullability contracts (similar to @Contract in Spring and JetBrains annotations).
  • Ways to simplify migration for large application codebases.

JSpecify’s evolution remains a community effort – open, vendor-neutral, and guided by real feedback.

We invite developers to share migration experiences and report issues by:

Together, we can make null safety not just a specification, but a reliable, everyday reality for the entire Java ecosystem.

Stay safe – and may your code always be @NonNull.

Recommended talks & resources

Here are some reliable and accessible resources that give an excellent introduction to JSpecify, its motivations, and practical usage:

Talks

  • JSpecify: Java Nullness Annotations and Kotlin — David Baker

A clear, practical introduction to JSpecify from one of the engineers directly involved in Kotlin’s null-safety tooling. The talk explains how JSpecify fits into the broader ecosystem, how Kotlin interprets JSpecify annotations, what benefits it brings to mixed Java/Kotlin codebases, and why typed nullness matters.

  • Null Safety in Spring Applications With JSpecify and NullAway — Sébastien Deleuze

A focused look at how Spring Framework 7 adopts JSpecify, how Spring Boot 4 integrates null-safety across the stack, and how NullAway is used to enforce consistent nullability contracts. Essential viewing for anyone migrating Spring applications or evaluating JSpecify adoption.

Articles & Documentation

  • Spring Framework 7 – Null Safety Documentation. Shows how Spring applies JSpecify across the framework and how developers should adjust their code.
  • NullAway — JSpecify Support Guide. Details the interaction between static analysis and JSpecify annotations.
image description

Discover more