skip to content

What is JSpecify, how does it improve Java-to-Kotlin nullability over older annotations, and how does Kotlin's strict mode treat JSpecify-annotated code?

level: seniorimportance: should knowfreq 38%

answer

  1. JSpecify = vendor-neutral standard (org.jspecify.annotations)
  2. @NullMarked flips scope to non-null-by-default
  3. TYPE_USE -> nullability inside generics & arrays
  4. Strict reporting -> compile-time errors, not platform leniency
  5. Under @NullMarked, unannotated -> non-null, platform types vanish

basics

~10 s

JSpecify is a standard, vendor-neutral set of nullability annotations for Java. Kotlin understands it, including module-wide defaults and generic type-argument nullability, and can treat any mismatch as a compile error in strict mode.

solid answer

~40 s

JSpecify (`org.jspecify.annotations`) is an industry-standard nullability spec — `@Nullable`, `@NonNull`, and crucially `@NullMarked` to flip a package/module to non-null-by-default. Its big advantage over older libraries (JetBrains, JSR-305) is **precise generics and type-use semantics**: the annotations are `TYPE_USE`, so you can express `List<@Nullable String>` and nullability on type parameters and array elements, which older declaration-site annotations couldn't. Kotlin (2.x) supports JSpecify and lets you escalate how strictly it's enforced via the `-Xjspecify-annotations=strict` / `-Xtype-enhancement-improvements-strict-mode` style flags (or `jsr305`/`jspecify` reporting levels), turning would-be platform-type leniency into hard **compile-time errors** when you violate a Java-declared contract. With `@NullMarked`, unannotated types in that scope become non-null, so platform types largely disappear and the boundary becomes as safe as pure Kotlin.

code

kotlin · 12 lines
kotlin
// Java with JSpecify:
//   @NullMarked
//   package com.example;
//   class Repo {
//     List<@Nullable User> findAll();   // list non-null, elements may be null
//     @Nullable User findById(long id); // result may be null
//     User require(long id);            // non-null by default (@NullMarked)
//   }

val all: List<User?> = repo.findAll()   // element nullability honored
val one: User? = repo.findById(1)       // nullable
val must: User = repo.require(1)         // non-null, no platform type

go deeper

for a junior

Recognizes JSpecify as a newer standard set of nullability annotations Kotlin understands.

for a middle

Explains @NullMarked defaulting and that Kotlin maps the annotations to nullable/non-null types.

for a senior

Articulates the TYPE_USE/generics advantage and how strict reporting turns violations into compile errors, removing platform-type leniency.

for a principal

Plans a migration: package-level @NullMarked rollout, strict-mode flags in CI, standardizing the org on JSpecify over legacy annotation flavors.

## The problem JSpecify solves Older nullability annotations (JetBrains, JSR-305, AndroidX) were **declaration-site** and inconsistent across vendors. They couldn't precisely express nullability **inside generics** — e.g. distinguishing `List<@Nullable String>` (list of maybe-null) from `@Nullable List<String>` (maybe-null list). They also varied in defaulting behavior. ## What JSpecify is **JSpecify** (`org.jspecify.annotations`) is a **vendor-neutral standard** backed by Google, JetBrains, and others, with three core pieces: - `@Nullable` / `@NonNull` — **`TYPE_USE`** annotations, so they attach to any type usage including type arguments and array components. - `@NullMarked` — applied to a module, package, class, or method to make **unannotated types non-null by default** within that scope. Inside a `@NullMarked` scope you only annotate the exceptions (`@Nullable`). - `@NullUnmarked` — opt a scope back out. ## Why TYPE_USE matters Because the annotations are type-use, the **full type structure** carries nullability: ```kotlin // Java (JSpecify): // @NullMarked // List<@Nullable String> namesWithGaps(); // list non-null, elements nullable // @Nullable List<String> maybeList(); // list nullable, elements non-null val gaps: List<String?> = obj.namesWithGaps() // element nullability preserved val maybe: List<String>? = obj.maybeList() // container nullability preserved ``` Kotlin maps these to the correct nullable positions instead of collapsing everything to a platform type. ## How Kotlin enforces it Kotlin recognizes JSpecify and applies a **reporting level**: `ignore`, `warn`, or `strict`. In **strict mode** a contract violation (e.g. passing `null` where a JSpecify `@NonNull` is required, or dereferencing a `@Nullable` without a check) is a **compile-time error**, not a silent platform-type pass. This is controlled by compiler flags (the `-Xjspecify-annotations` / type-enhancement strict-mode family) and is the direction Kotlin is moving toward by default. ## Effect on platform types Under `@NullMarked`, **unannotated** Java types become **non-null** in Kotlin rather than platform types. The `String!` ambiguity largely vanishes for that code, so the boundary behaves like ordinary type-safe Kotlin — the late-NPE risk is designed out. ## Practical guidance - For new owned Java, prefer JSpecify + `@NullMarked` at the package level. - Turn on strict reporting once a module is fully marked, so regressions fail the build. - It composes with Kotlin's existing recognition of other annotation libraries.

  • What does @NullMarked do that plain @NotNull on each member does not?
    It flips the default for a whole scope to non-null, so you annotate only the nullable exceptions — far less boilerplate and no forgotten members leaking platform types.
  • Why couldn't older annotations express List<@Nullable String>?
    They were declaration-site annotations, so they applied to the whole declaration, not to inner type arguments; JSpecify uses TYPE_USE targets that attach to any type position.

Old annotations labeled the box; JSpecify labels every item inside the box and lets you declare 'everything in this warehouse is non-fragile unless tagged'.

saying these in an interview costs you the question

  • Confusing JSpecify with a runtime null-check framework
  • Saying older annotations already handled generic element nullability
  • Thinking @NullMarked makes things nullable rather than non-null-by-default
  • Claiming JSpecify is JetBrains-proprietary
  • Assuming strict enforcement is always on by default in current Kotlin

context