Skip to content

JSpecify Support

Vladimir Sitnikov edited this page Sep 5, 2026 · 27 revisions

JSpecify is an effort to standardize annotations for Java static analysis, and JSpecify 1.0 includes nullability annotations. Out of the box, NullAway supports JSpecify annotations in its standard mode of checking. If you are a current NullAway user, you should just be able to swap in JSpecify annotations for whatever nullability annotations you were using before, and you should not get any new NullAway errors. If you see problems, report an issue.

Type-Use Annotation Placement

NullAway requires that JSpecify annotations be written in the correct place on qualified and array types, both in and out of JSpecify mode. So, e.g., if you have a field String[] x, to mark x itself as @Nullable you must write String @Nullable [] x; @Nullable String[] x annotates the element type instead. And, if you have a varargs parameter foo(Object... args) and you would like the args array itself to be @Nullable, you must write foo(Object @Nullable... args). This applies to JSpecify's @Nullable annotation and to any other type-use @Nullable annotation. See JSpecify's type-use annotation syntax for further details on the syntax itself, and the discussion on PR #1010 and PR #1025 for the gory details on how NullAway interprets different types of annotations.

NullAway 0.12.0 introduced this requirement, and upgrading to it may lead to NullAway reporting new errors in existing code. Versions 0.12.0 through 0.13.8 provided a compatibility flag -XepOpt:NullAway:LegacyAnnotationLocations to use NullAway's old logic for interpreting annotations to ease the transition; the flag was removed in version 0.14.0.

JSpecify Mode

Pass -XepOpt:NullAway:JSpecifyMode=true to check full JSpecify semantics for these annotations, including annotations on generic types. This mode is still under development and may report false positive warnings on your code. Report these cases and we will address them.

Further JSpecify support is off by default, because it leads to many new errors in existing projects and because it needs more real-world testing. Pass -XepOpt:NullAway:JSpecifyExperimental=true alongside JSpecifyMode=true to turn it on. As of version 0.14.0 it enables:

Each feature also has its own flag, so you can enable them one at a time. Most of the new errors come from the JDK models. We expect to enable JSpecifyExperimental by default in a future release, and we encourage projects to enable it now and report any issues that arise.

Major outstanding features still to be supported include:

  • full inference support for generic methods
  • checking of generic class implementations (i.e., checking that generic type variables are used correctly). Thus far we have focused on supporting uses of generic classes, but not on checking their internals.

Not supporting the above features may lead to false negatives (missed issues). We are slowly working towards supporting them, and any help from the community is appreciated, from issue reports to pull requests.

Supported JDK versions

JSpecify mode requires a javac that reads type use annotations from bytecodes, which is important for predictable and complete checking behavior. Either (1) the running javac is from JDK 22 or higher, or (2) the -XDaddTypeAnnotationsToSymbol=true flag has been passed to javac. As of version 0.12.11 (see PR #1245), NullAway checks this and fails the build otherwise, with an IllegalStateException:

Running NullAway in JSpecify mode requires either JDK 22+ or passing the flag -XDaddTypeAnnotationsToSymbol=true to an older JDK that supports it; see https://github.com/uber/NullAway/wiki/JSpecify-Support#supported-jdk-versions for details.

The -XDaddTypeAnnotationsToSymbol=true flag enables support for type use annotations in bytecodes for JDK 21 as of release 21.0.8, and JDK 17 as of release 17.0.19. Note that the -XDaddTypeAnnotationsToSymbol=true flag is not supported by Oracle JDK 21 or 17; you must use an OpenJDK build like those from Temurin or Zulu. We recommend using the latest javac version possible when building your code to get all the latest bug fixes; the --release flag enables targeting older JDK versions while still using the latest compiler.

RequireExplicitNullMarking Checker

As of version 0.12.13, NullAway includes a RequireExplicitNullMarking Error Prone checker (off by default) that reports a top-level class unless the class or its package is explicitly @NullMarked or @NullUnmarked, or its module is explicitly @NullMarked. Only top-level classes are reported. For those using the OnlyNullMarked NullAway setting, this check helps ensure that classes have some explicit @NullMarked or @NullUnmarked annotation and do not end up @NullUnmarked by default. (NullAway performs no checks inside @NullUnmarked code.)

To use the checker, set it to warning or error level via Error Prone flags / your build system: -Xep:RequireExplicitNullMarking:WARN or -Xep:RequireExplicitNullMarking:ERROR. At its default SUGGESTION severity it reports nothing at all, so that a user who has not asked for it sees no diagnostics. A report on an uncovered class carries the text:

Top-level classes must either be directly annotated with @NullMarked/@NullUnmarked, be in a package that is explicitly @NullMarked/@NullUnmarked, or be in a module that is explicitly @NullMarked.

JSpecifyUnrecognizedAnnotationLocation Checker

As of version 0.14.2, NullAway includes a JSpecifyUnrecognizedAnnotationLocation Error Prone checker (off by default) that checks that JSpecify @Nullable and @NonNull annotations only appear in recognized locations, as defined by the JSpecify specification. This check can aid in preventing confusing code where these annotations appear in the locations where they will not be read by JSpecify checking tools, such as on a primitive type, on the root type of a cast, or on a class declaration.

To use the checker, set it to warning or error level via Error Prone flags / your build system: -Xep:JSpecifyUnrecognizedAnnotationLocation:WARN or -Xep:JSpecifyUnrecognizedAnnotationLocation:ERROR. At its default SUGGESTION severity it reports nothing at all. A report carries the text below, naming the location it found out of the 19 the check distinguishes:

A nullness annotation <location> has no meaning under JSpecify.
A nullness annotation on a primitive type has no meaning under JSpecify.
A nullness annotation directly on a wildcard has no meaning under JSpecify.

Reports carry a suggested fix that moves the annotation to the recognized location the author meant, or removes it where no such location can be named unambiguously, so Error Prone's patch mode can apply them in bulk.

The check reads org.jspecify.annotations.Nullable and org.jspecify.annotations.NonNull, and no other nullness annotation, because the rule is JSpecify's and JSpecify states it about its own annotations. Other tools give their own @Nullable a meaning in some of these locations: the Checker Framework reads @Nullable on the root type of a local variable and of a cast, and IntelliJ reads the JetBrains @Nullable on a local variable.

The check has a single configuration flag, CheckLocalVariableRootType, to configure whether warnings are reported for annotations on the root type of a local variable (an unrecognized location according to the spec). E.g., for @Nullable String s = ..., the @Nullable annotation is unrecognized when s is a local variable, but List<@Nullable String> l = ... is fine. The checking is on by default. To disable it, pass -XepOpt:JSpecifyUnrecognizedAnnotationLocation:CheckLocalVariableRootType=false. The flag carries the check's own name rather than NullAway's.

JSpecify and Guava

As of version 33.4.1, the Guava library uses JSpecify @NullMarked annotations for most of its packages and classes. This means that NullAway will automatically perform stricter checking of calls into Guava code, whether or not NullAway is run in JSpecify mode. Outside of JSpecify mode, NullAway may report new false positive errors, as it does not allow @Nullable expressions to be passed to Guava methods whose parameter type is a type variable. (See discussion, e.g., in uber/NullAway#1199, uber/NullAway#1186, or PicnicSupermarket/error-prone-support#1598.) Running NullAway in JSpecify mode should remove these false positives, but it may require annotating type arguments as @Nullable, including in cases where javac infers the type arguments and NullAway's inference cannot infer their nullability (see uber/NullAway#1075). To maximize null safety, we recommend running NullAway in JSpecify mode with additional annotations, but you may still need to suppress some false positives. Alternately, if you do not want to switch to JSpecify mode yet, you will have to suppress some false positives when updating to a recent Guava version.

Clone this wiki locally