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?
answer
- JSpecify = vendor-neutral standard (org.jspecify.annotations)
- @NullMarked flips scope to non-null-by-default
- TYPE_USE -> nullability inside generics & arrays
- Strict reporting -> compile-time errors, not platform leniency
- Under @NullMarked, unannotated -> non-null, platform types vanish
basics
~10 sJSpecify 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 sJSpecify (`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// 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 typego deeper
Recognizes JSpecify as a newer standard set of nullability annotations Kotlin understands.
Explains @NullMarked defaulting and that Kotlin maps the annotations to nullable/non-null types.
Articulates the TYPE_USE/generics advantage and how strict reporting turns violations into compile errors, removing platform-type leniency.
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