🤔 Should You Upgrade Now? Link to heading
Kotlin 2.4.0 shipped on June 3, 2026. If 2.3.20 was the tooling half of the cycle, this is the language half — and it’s mostly a finishing release. Features that were experimental in 2.2 and 2.3 are now stable, and warnings that have sat in your build log for a release or two are now errors.
The biggest reason to move is stable context parameters. No opt-in anymore, so a feature you kept out of production because it needed a compiler flag is now fair game.
A service that’s happy on 2.3.x and doesn’t want new language features can wait — but waiting won’t make the upgrade smaller. Nearly every entry in the 2.4 compatibility guide follows one pattern: “2.2.0: report a warning. 2.4.0: raise the warning to an error.” None of them are surprises if you read your warnings. All of them are surprises if you don’t, and they pile up. If your build log has been noisy for a while, 2.4.0 is where that bill arrives.
💸 The Three Things That Will Actually Cost You Time Link to heading
🏷️ 1) Your annotations land somewhere new Link to heading
This is the one I’d respect most, because it can change runtime behavior without a single compile error.
When you put an annotation on a Kotlin property without a target like @field: or @param:, the compiler picks one. In 2.4.0 the new rule is on by default: it uses param and property if they apply, and uses field only if property doesn’t apply. The compatibility guide marks this as a binary change that “can affect annotation processing, reflection, and binary metadata after recompilation.”
For Bean Validation, that’s concrete. Jakarta’s constraints can’t target a Kotlin property, so on a constructor property they now land on both the parameter and the field:
data class SearchRequest(
// Kotlin 2.4.0: equivalent to @param:NotBlank @field:NotBlank
@NotBlank val query: String,
)
Under the old rule, that annotation only went to the constructor parameter. That’s why I wrote in Validating HTTP Arguments Safely in Kotlin that you need @field: for validation to fire.
Here’s the risk: if anyone forgot the @field: prefix, that validation was silently off. After the upgrade it may switch on — arguably a fix, but still a behavior change, and it’ll show up as requests that used to pass now failing with a 400.
If you followed my Spring Boot 4 upgrade path and added -Xannotation-default-target=param-property, you’ve been on this rule since then. That part of the upgrade is already behind you.
⚠️ 2) Warnings that are now errors Link to heading
Most of the compatibility guide is warnings turning into errors. These are the ones I’d expect to touch backend code:
- K1 and
-language-version=1.9are gone. If any module still pins language version 1.9, it no longer compiles. - Jakarta nullability annotations are enforced. Assigning a value from a Java declaration marked
@jakarta.annotation.Nullableto a non-null Kotlin type is now an error. - Explicit nullable type arguments for Java types are no longer flexible. Code that leaned on that looseness when calling Java APIs now gets errors.
- Exhaustive
whenon non-abstract Java sealed classes needs anelsebranch or a branch for the sealed class itself.
There are a few more (inline functions exposing lower-visibility declarations, always-false is checks), but they’re rarer. It’s the kind of list that turns a “quick version bump” into an afternoon.
🔧 3) Removed Kotlin Gradle plugin APIs Link to heading
If you have custom build logic, check it. 2.4.0 removes a list of long-deprecated Kotlin Gradle plugin APIs, including KotlinJvmCompile.moduleName, parentKotlinOptions, KotlinCompilation.compileKotlinTask, and KaptExtension.processors. Convention plugins and old buildSrc code are where these tend to hide — they’re easy to forget because they rarely change.
🛤️ The Path Link to heading
Every code change below is its own PR, and the first two land on 2.3.x before you touch the compiler version at all.
1. Clear your warnings on 2.3.x. Most 2.4.0 errors are 2.3.x warnings, so this is where the real work happens. Check that no module sets -language-version=1.9, and rename any overloads that differ only by context parameters — 2.3.20 made those ambiguous.
2. Decide what your annotations should do. Either pin the target with @field: wherever validation or serialization depends on it — that behaves the same on both compiler versions — or accept the new rule and plan to test hard after the bump. My default is to pin anything that guards an HTTP boundary.
If you need the old behavior project-wide while you sort it out, there’s an escape hatch:
kotlin {
compilerOptions {
// temporary: keep the pre-2.4 annotation target rule
freeCompilerArgs.add("-Xannotation-default-target=first-only")
}
}
Treat it as temporary. Explicit targets like @field:NotBlank work either way, so that’s where you want to end up.
3. Confirm your tools support 2.4.0 — annotation processors, code generators, static analysis. On Gradle, 2.4.0 is fully compatible with 7.6.3 through 9.5.0.
4. Bump to 2.4.0 and fix custom Gradle logic. Replace any removed KGP APIs with what their deprecation diagnostics suggest.
5. On Maven, let the plugin align the JVM target. With <extensions>true</extensions>, kotlin-maven-plugin now derives the JVM target from maven.compiler.release (or maven.compiler.target if release isn’t set):
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>2.4.0</version>
<extensions>true</extensions>
</plugin>
It only reads project properties and the global maven-compiler-plugin configuration, not settings inside <executions> — so remove a hand-set kotlin.compiler.jvmTarget only after confirming the derived one matches.
6. Run your validation and serialization tests — with bad input. That’s where step 2’s decision proves itself.
✅ How You Know You’re Done Link to heading
The build compiles with no first-only flag and zero warnings. That second part matters — the next language release will do this all over again. A couple are already queued: property references to javaClass are deprecated (use .javaClass or ::class.java), and readLine() is deprecated in favor of readln() and readlnOrNull().
The tests to trust send bad input to a real endpoint — missing fields, blank strings, out-of-range values. They tell you whether the annotation change switched anything on. Add serialization tests and anything that reads annotations at runtime.
In staging, compare 400 rates per endpoint against the previous build — a validation that switched on shows up there before it shows up in a support ticket.
Rolling back is simple, because the compiler bump is one PR. The annotation change only takes effect after recompilation, so reverting and rebuilding puts things back — and anything you pinned in step 2 behaves the same either way.
🎁 Everything Else Link to heading
The release notes have the full list, but a few nice things are waiting once you’re there. Context parameters are stable. Explicit context arguments (-Xexplicit-context-arguments) let you resolve the overload ambiguity from 2.3.20 at the call site, but they’re still experimental, so I’d rename first. I’ll cover how I’d use context parameters in a Spring service in a separate post. Explicit backing fields are stable, so val items: List<String> with field = mutableListOf() replaces the old _items / items pattern. kotlin.uuid.Uuid is stable too, though Uuid.generateV7() still needs opt-in and Uuid can order differently from java.util.UUID. And isSorted() finally replaces those hand-rolled zipWithNext().all { ... } checks.
🧹 Conclusion Link to heading
Kotlin 2.4.0 is a cleanup release dressed as a language release. Stable context parameters are the real news, the annotation default is the thing to respect, and the warnings-to-errors list is long but fair.
If you’ve kept your build log clean, you’re most of the way there. If you haven’t, this is a good excuse to start — clear the warnings, pin the annotations that guard your HTTP boundary, and let the tests tell you when you’re done. Future you, staring at the 2.5 compatibility guide, will be grateful.
Sources
- What’s new in Kotlin 2.4.0
- Kotlin 2.4.0 Released (Kotlin blog)
- Compatibility guide for Kotlin 2.4
- Kotlin releases
- Context parameters
- Annotations: use-site targets
- Properties: explicit backing fields
- UUIDs
- What’s new in Kotlin 2.3.20
Related Posts