Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Kotlin  Module 3 – Null safety

Platform types and Java nullability

See why values from Java reach Kotlin with platform types, where a null from Java throws, and how JSpecify annotations turn them into checked types.

  • Intermediate
  • 15 minutes
  • Examples run with Kotlin 2.4.20, Java temurin-25.0.4+101.0.LTS and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 6 of 15 minutes free
Free: A value from unannotated Java has a platform type · Nullability annotations turn platform types into real types. With a pass: 2 in-depth parts and 3 interview answers, about 9 minutes.

What you will learn

  • Explain platform types, shown as String!, and why the compiler lets every call on them through
  • Predict where a null from Java fails when it reaches a Kotlin variable, a parameter or a list
  • Use JSpecify annotations so that nulls from Java are checked while Kotlin code compiles
  • Wrap an unannotated Java class in a Kotlin adapter whose public types say exactly what can be null
  • Plan the move of a Java code base to JSpecify with @NullMarked packages and the compiler's warn mode

Before you start

What you’ll learn · Before you start

What you will learn

  • Explain platform types, shown as String!, and why the compiler lets every call on them through
  • Predict where a null from Java fails when it reaches a Kotlin variable, a parameter or a list
  • Use JSpecify annotations so that nulls from Java are checked while Kotlin code compiles
  • Wrap an unannotated Java class in a Kotlin adapter whose public types say exactly what can be null
  • Plan the move of a Java code base to JSpecify with @NullMarked packages and the compiler's warn mode

Before you start

On this page

A Java method declared as String findCity(String pin) returns a city for three PIN codes and null for every other one, and nothing in its signature says so. Kotlin cannot see inside Java code, so it neither trusts the result nor distrusts it: it gives the value a platform type: The type Kotlin gives a value from Java code without nullability annotations, shown as String! in messages. It may or may not hold null, and the compiler lets the code treat it either way., written String!, and leaves the decision to you.

You will see what each decision does when a null arrives, make Java code state its nullability with JSpecify annotations, and wrap a Java class you cannot change behind a Kotlin adapter with exact types.

A value from unannotated Java has a platform type

PinDirectory is a Java class without any nullability annotation. The session compiles it with javac, compiles a Kotlin file against it with kotlinc, and runs the result:

PinDirectory.java used from Kotlin in four ways Python · platform_session.py
from session import run

# A Java class without nullability annotations, compiled with javac, then used from Kotlin in four ways; and one
# deliberate type mismatch, so that the compiler's message shows the platform type String!.
run("""
cd plain
javac -d out PinDirectory.java
kotlinc -cp out Main.kt -include-runtime -d main.jar
java -cp main.jar:out MainKt
!kotlinc -cp out Show.kt -d show
""")

Output

$ cd plain
$ javac -d out PinDirectory.java
$ kotlinc -cp out Main.kt -include-runtime -d main.jar
$ java -cp main.jar:out MainKt
110001: New Delhi, 9 letters
999999 as String?: no city
inferred: java.lang.NullPointerException: Cannot invoke "String.length()" because "city" is null
declared String: java.lang.NullPointerException: findCity(...) must not be null
$ kotlinc -cp out Show.kt -d show
Show.kt:3:21: error: initializer type mismatch: expected 'Int', actual 'String!'.
    val digits: Int = PinDirectory().findCity("110001")
                    ^
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 platform_session.py

delhi.length compiled without a ?., although findCity can return null: the compiler allows every call on platform types and reports nothing. The last command is a deliberate mistake whose message names the type it gave the value, String!, meaning “String or String?, the compiler does not know which”. You cannot write String! yourself; it appears only in messages and in the editor’s hints.

Platform types leave the decision to you, and the decision is the type your code gives the value. Step through the PIN code 999999, which the directory does not know:

Interactive · message by message Where a null from Java turns into an exception Step through the calls of Main.kt for the PIN code 999999, which the directory does not know. Watch where each version fails, if it fails at all.
Where a null from Java turns into an exception, step 6 of 6. Messages so far: Main.kt to PinDirectory.java: findCity("999999"); PinDirectory.java to Main.kt: null, a String!; Main.kt to Kotlin's null check: is it null?.Main.ktyour Kotlin codeKotlin's null checkadded by the compilerPinDirectory.javano annotationsfindCity("999999")null, a String!is it null?

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The call. Main.kt calls findCity("999999"). The Java method looks the PIN code up in its map, finds nothing and returns null. Its signature says only String, so Kotlin gives the result the platform type String!.
  2. Java returns null. The null comes back to Kotlin. Nothing has failed yet: the type that the Kotlin code gives the value decides where, or whether, it fails.
  3. Declared String?. In the version declared String?, nothing is checked and nothing fails: the compiler made the code handle null with ?. and ?:, and it printed no city.
  4. Inferred, then used. In the inferred version, city keeps the platform type and the code calls city.length. The JVM finds null there and throws: Cannot invoke "String.length()" because "city" is null.
  5. Declared String. In the version declared String, the compiler added a null check right after the call. The check sees null and throws at once: findCity(...) must not be null, which names the Java call.
  6. The difference. Both exceptions are NullPointerExceptions, but the declared type moved the failure to the boundary with Java and named the call, while the inferred one failed later, wherever the value happened to be used.

A variable declared String? is always safe: the compiler then asks for ?. or ?:, and the program printed “no city”. A variable declared String is checked by Kotlin when it is assigned, so a null fails there, with a message that names the Java call. A variable with an inferred type keeps the platform type and fails later, wherever it is used. The coding conventions turn this into a rule for public code: a function that returns a value of a platform type declares its Kotlin type, so that the decision is made once, where the value enters Kotlin.

Key idea

Give every value from unannotated Java its Kotlin type where it enters Kotlin: String? when the Java code can return null, String only when it cannot, and never a platform type that spreads through inferred types.

Checkpoint

Check: Platform types, and where a Java null fails

  1. Question 1 of 2 In Main.kt, delhi.length compiled without a safe call, although findCity can return null. Why?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: findCity is unannotated Java, so its result has a platform type, and the compiler allows every call on it

    A platform type, shown as String!, means String or String?, the compiler does not know which, so it lets every use through and the null, if there is one, fails while the program runs.

  2. Question 2 of 2 val city: String = directory.findCity("999999") runs, and the directory has no such PIN code. What happens, and where?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A NullPointerException at the assignment: findCity(...) must not be null

    The type you give a Java value decides where a null fails: String? makes you handle it, String fails at the assignment with a message, and an inferred type fails wherever the value is first used.

Nullability annotations turn platform types into real types

Java nullability can be written into the Java code itself, as nullability annotation: An annotation in Java code, such as JSpecify's @Nullable, that says whether a type may hold null. Kotlin reads it and sees the Java type as a real nullable or non-null type. that Kotlin reads. An annotated type reaches Kotlin as a real String? or String, and the compiler checks every use. JSpecify is a common set of such annotations for JVM languages: inside a class or package marked @NullMarked: A JSpecify annotation on a class, a package or a module: inside it, every type that has no @Nullable is non-null., a type without @Nullable is non-null. Here is the same directory with two annotations, used from the code that compiled before:

The directory annotated with JSpecify 1.0.0 Python · annotated_session.py
from session import library, run

# The directory annotated with JSpecify 1.0.0, whose jar is checked against the SHA-256 published on Maven Central.
library("org.jspecify:jspecify:1.0.0", "marked/lib")
run("""
cd marked
shasum -a 256 lib/jspecify-1.0.0.jar
javac -cp lib/jspecify-1.0.0.jar -d out PinDirectory.java
!kotlinc -cp out:lib/jspecify-1.0.0.jar Broken.kt -d broken
kotlinc -cp out:lib/jspecify-1.0.0.jar Fixed.kt -include-runtime -d fixed.jar
java -cp fixed.jar:out:lib/jspecify-1.0.0.jar FixedKt
""")

Output

$ cd marked
$ shasum -a 256 lib/jspecify-1.0.0.jar
1fad6e6be7557781e4d33729d49ae1cdc8fdda6fe477bb0cc68ce351eafdfbab  lib/jspecify-1.0.0.jar
$ javac -cp lib/jspecify-1.0.0.jar -d out PinDirectory.java
$ kotlinc -cp out:lib/jspecify-1.0.0.jar Broken.kt -d broken
Broken.kt:5:18: error: only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type 'String?'.
    println(delhi.length)
                 ^
Broken.kt:6:22: error: initializer type mismatch: expected 'String', actual 'String?'.
    val city: String = directory.findCity("999999")
                     ^
Broken.kt:8:32: error: null cannot be a value of a non-null type 'String'.
    println(directory.findCity(null))
                               ^^^^
[exit status 1]
$ kotlinc -cp out:lib/jspecify-1.0.0.jar Fixed.kt -include-runtime -d fixed.jar
$ java -cp fixed.jar:out:lib/jspecify-1.0.0.jar FixedKt
110001: 9 letters
999999: unknown PIN code

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 annotated_session.py

All three lines of Broken.kt would compile against the plain class, and all three are refused now, each with the message of an ordinary Kotlin type: findCity gives a String?, so delhi.length and the assignment to a String are refused, and its parameter is a non-null String, so findCity(null) is refused too. The fixed version handles null where the value arrives. The first command printed the SHA-256 of the JSpecify 1.0.0 jar, so you can check that your copy is the same file.

Kotlin reads several families of nullability annotations, and Java nullability reached more code in stages:

Timeline · 5 events in order How Java code tells Kotlin about null Walk through the stages in order. Each one made more Java types reach Kotlin as real nullable or non-null types.
  1. Annotations Kotlin reads

    One annotation per library family no platform type

    Kotlin reads the nullability annotations of several families, among them JetBrains' org.jetbrains.annotations, Android's, JSR-305's javax.annotation, Eclipse's, Lombok's and RxJava's. An annotated Java type reaches Kotlin as a real type, not a platform type.

  2. Android 9 (API 28)

    The Android SDK annotated warnings first

    The Android 9 SDK added nullability annotations to some commonly used APIs. For a while, Kotlin reported mismatches with these new annotations as warnings, not errors, so that code which compiled before kept compiling.

  3. JSpecify 1.0.0

    A common set of annotations one standard

    JSpecify, made by several organisations together, gives JVM languages one set of nullness annotations with a precise meaning: @Nullable, @NonNull, @NullMarked and @NullUnmarked.

  4. Kotlin 2.1.0

    JSpecify mismatches become errors strict by default

    From Kotlin 2.1.0 on, the compiler reports nullability mismatches with JSpecify annotations as errors by default, where earlier versions reported them as warnings.

  5. Spring Framework 7

    A large framework moves over JSpecify in use

    Spring Framework 7's code is annotated with JSpecify, usually with @NullMarked on each package, and its own older nullability annotations are deprecated. Kotlin code sees its APIs with real nullable and non-null types.

The timeline has 5 events, from Annotations Kotlin reads to Spring Framework 7.

Since Kotlin 2.1.0, a JSpecify mismatch is an error by default, which is why Broken.kt failed instead of compiling with warnings. A Java library you call may already carry these annotations: the Spring Boot documentation says that most Spring projects provide null safety through JSpecify, and Android’s interop guide asks every public Java API to annotate the nullability of its parameters, returns and fields.

Checkpoint

Check: Nullability annotations and JSpecify

  1. Question 1 of 2 Inside a @NullMarked class, Java declares public @Nullable String findCity(String pin). How does Kotlin see the method?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: fun findCity(pin: String): String?

    With JSpecify, @Nullable marks what may be null and @NullMarked makes everything else non-null, so Kotlin sees real types and checks each use while compiling.

  2. Question 2 of 2 What does @NullMarked on a Java class mean for a type in it that carries no annotation?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It is non-null

    @NullMarked flips the default: inside it, plain types are non-null, and only @Nullable types may hold null.

Where Kotlin checks values at the Java boundary, measured Pro: opens with a Pro, Premium or Ultimate pass

Kotlin cannot stop Java from passing or returning null, so the compiler writes checks where values cross from one language to the other. This part finds those checks in the bytecode, measures what each one throws, and shows the case that gets past all of them.

In this part:

  • The checks the compiler writes, found with javap
  • Two compiler options leave these checks out
  • A null inside a list gets past every check
  • The JVM’s messages and Kotlin’s

1 runnable example (Python) · about 5 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Moving a Java code base to JSpecify, step by step Pro: opens with a Pro, Premium or Ultimate pass

Annotating a large Java code base in one change is rarely possible, and every annotation changes what the Kotlin code that calls it may compile. This part marks one package, leaves a sub-package for later, and uses the compiler’s warn mode to move without a broken build.

In this part:

  • @NullMarked on a whole package
  • @Nullable on the items of a list
  • The warn mode keeps the build green while callers move
  • Annotate, wrap or decide at each call

1 runnable example (Python) · about 7 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

Practise: an adapter with exact types

LegacyDirectory is an old Java class that you cannot change, and it returns null in two ways without saying so. CityLookup.kt wraps it in a Kotlin class, and as given, three of its four checks fail:

CityLookup.kt: four checks, three of them fail Python · practice_session.py
from session import run

# The practice: an old Java class, and the Kotlin adapter whose checks fail as it is given.
run("""
cd practice
javac -d out LegacyDirectory.java
kotlinc -cp out CityLookup.kt -include-runtime -d lookup.jar
!java -cp lookup.jar:out CityLookupKt
""")

Output

$ cd practice
$ javac -d out LegacyDirectory.java
$ kotlinc -cp out CityLookup.kt -include-runtime -d lookup.jar
$ java -cp lookup.jar:out CityLookupKt
ok   city("110001") = New Delhi, want New Delhi
FAIL city("999999") = NullPointerException, want null
FAIL cities("Goa") = NullPointerException, want []
FAIL cities("Kerala") = [Kochi, null, Thiruvananthapuram], want [Kochi, Thiruvananthapuram]
1 of 4 checks pass
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 practice_session.py

Copy the two files, give CityLookup exact types, and run the four commands of the session until all four checks pass. First hint: city should say in its return type that the city may be unknown, and a platform list can be cleaned with orEmpty() and filterNotNull() before it leaves the adapter.

Then test what you know of platform types and of the annotations that replace them. The questions start with the idea your answers show you know least:

Practice that adapts to you

Practise this lesson

5 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 5 Can you declare a variable as val city: String! in Kotlin?

    Choose one answer.

    Show the answer: Question 1 of 5

    Answer: No; String! is how the compiler shows a platform type, and only messages and the editor show it

    A platform type is something the compiler gives a Java value, never a type you write. You write String or String? to make the decision it leaves to you.

  2. Question 2 of 5 A public Kotlin function is written fun cityOf(pin: String) = directory.findCity(pin), with no return type, and directory is unannotated Java. What do its callers get, and what should change?

    Choose one answer.

    Show the answer: Question 2 of 5

    Answer: A platform type, String!; declare the return type, String? or String, as the coding conventions ask

    Platform types spread through inferred types. Give every public function that returns a value from Java an explicit Kotlin type.

  3. Question 3 of 5 In the practice, LegacyDirectory.findCity returns null for an unknown PIN code. Which return type should CityLookup.city have?

    Choose one answer.

    Show the answer: Question 3 of 5

    Answer: String?, so that callers must handle an unknown PIN code

    An adapter turns a Java class's unknown nullability into exact Kotlin types, so its callers get the compiler's checks back.

  4. Question 4 of 5 Which Java declaration makes Kotlin refuse directory.findCity("110001").length at compile time?

    Choose one answer.

    Show the answer: Question 4 of 5

    Answer: public @Nullable String findCity(String pin), with JSpecify's @Nullable

    Only an annotation that Kotlin reads changes the type it gives a Java value. JSpecify's @Nullable is one of them.

  5. Question 5 of 5 Why did Broken.kt fail to compile instead of compiling with warnings?

    Choose one answer.

    Show the answer: Question 5 of 5

    Answer: Since Kotlin 2.1.0, the compiler reports JSpecify nullability mismatches as errors by default

    Kotlin 2.1.0 raised nullability mismatches with JSpecify annotations from warnings to errors by default, so Broken.kt stopped at the compiler instead of compiling with warnings.

Interview questions

  1. Fresher to mid · Warm-up

    What is a platform type in Kotlin, and how should you handle a value that has one?

    A platform type is the type Kotlin gives a value from Java code that has no nullability annotations. The compiler shows it as String!, meaning String or String?, it cannot tell which, and it lets your code treat the value either way without a check. Handle it by deciding where the value enters Kotlin: declare String? when the Java method can return null, and the compiler makes you handle it with ?. or ?:; declare String only when it never can, and Kotlin checks the value at the assignment, failing with a message such as findCity(...) must not be null. Do not let the platform type spread through inferred types, and where you own the Java code, annotate it so that Kotlin sees real types.

  2. Mid to senior

    Java code in your service calls a Kotlin function fun register(name: String) and sometimes passes null. Where exactly does it fail, what does the error tell you, and how would you design the Kotlin side for its Java callers?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Fresher to mid

    A Java method returns a List<String> that sometimes holds null items. Kotlin code calls names.map { it.length } on the result and crashes. Where does it fail, why did the compiler not catch it, and what should the Kotlin side do?

    Model answer, Pro. First step and follow-ups

  4. Senior to staff

    Your Kotlin services depend on a large Java library, owned by another team, with no nullability annotations, and its values cause most of your NullPointerExceptions. Would you annotate it, wrap it, or declare types at every call? Plan the change.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A value from unannotated Java has a platform type, shown as String! in messages, and the compiler allows every call on it, so a null fails while the program runs.
  • Give a Java value its Kotlin type where it arrives: String? makes the compiler ask for ?. or ?:, and String fails at the assignment with a message that names the Java call.
  • With nullability annotations such as JSpecify’s @NullMarked and @Nullable, Java types reach Kotlin as real String? and String, and since Kotlin 2.1.0 a JSpecify mismatch is a compile error.

Terms in this lesson

@NullMarked
A JSpecify annotation on a class, a package or a module: inside it, every type that has no @Nullable is non-null.
nullability annotation
An annotation in Java code, such as JSpecify's @Nullable, that says whether a type may hold null. Kotlin reads it and sees the Java type as a real nullable or non-null type.
platform type
The type Kotlin gives a value from Java code without nullability annotations, shown as String! in messages. It may or may not hold null, and the compiler lets the code treat it either way.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.