Kotlin Name-Based Destructuring: The Silent Bug Fix Most Devs Missed
TL;DR
Kotlin’s data class destructuring has been position-based since day one. Reorder your properties, and every destructuring call silently breaks. No compiler error, no warning, just wrong values assigned to wrong variables. Kotlin 2.3.20 ships an experimental name-based destructuring syntax that fixes this. It’s not stable yet, so here’s what you should do today and what to watch for tomorrow.
The problem: position-based destructuring is a landmine
The most dangerous bugs are the ones that compile cleanly. I’ve been bitten by this one personally.
Consider a simple data class:
data class User(val name: String, val email: String)
val (name, email) = getUser()
This works perfectly until someone refactors the data class:
data class User(val email: String, val name: String)
Now name holds the email and email holds the name. The compiler says nothing. Your tests might not catch it if they don’t assert on specific values. This bug ships to production.
Both name and email are String types, so there’s no type mismatch to flag. The destructuring maps to component1() and component2(), pure positional access. Swap the properties, and every destructuring site silently breaks.
Why this hits Android and KMP teams hard
Data classes are everywhere in Android and Kotlin Multiplatform codebases: API response models, UI state holders, domain entities. A single property reorder during a refactor can corrupt data flow across layers without a single compiler warning.
The fix: name-based destructuring in Kotlin 2.3.20
Kotlin 2.3.20 introduces an experimental name-based destructuring syntax that binds variables to property names instead of positions:
val (val mail = email, val username = name) = getUser()
Here, mail is bound to the email property and username is bound to the name property, regardless of their declaration order in the data class. Reorder the properties all you want; the bindings hold.
You can also match names directly without renaming:
val (val email, val name) = getUser()
Each variable binds to the property with the same name. Order in the destructuring expression doesn’t matter.
Enabling the feature
Since this is experimental, you need to opt in via a compiler flag:
// In build.gradle.kts
kotlin {
compilerOptions {
freeCompilerArgs.add("-XXLanguage:+NameBasedDestructuring")
}
}
Position-based vs. name-based: a comparison
| Aspect | Position-based (current) | Name-based (experimental) |
|---|---|---|
| Binding strategy | Maps to component1(), component2(), etc. | Maps to property name |
| Resilience to reordering | Breaks silently | Safe |
| Renaming a variable | Always allowed | Use val alias = propertyName syntax |
| Compiler support | Stable since Kotlin 1.0 | Experimental in 2.3.20+ |
| Requires opt-in | No | Yes (compiler flag) |
| Type safety on reorder | Only if types differ | Always safe |
The row that matters most is resilience to reordering. Position-based destructuring gives you zero protection when properties share a type. Name-based destructuring removes the problem entirely.
What you should do today
Here’s what most teams get wrong: they see “experimental” and ignore it. But position-based destructuring is in your codebase right now, and it’s already risky.
Until name-based destructuring is stable, use explicit property access:
// Instead of this:
val (name, email) = getUser()
// Do this:
val user = getUser()
val name = user.name
val email = user.email
Yes, it’s more verbose. But it’s refactor-proof today, without any experimental flags. In codebases I’ve worked on, we lint against destructuring data classes with more than two same-typed properties for exactly this reason.
For teams on Kotlin 2.3.20+ who want to experiment, enable the flag in a feature branch and migrate incrementally. Don’t adopt it in production modules until it reaches stable status. Experimental language features can change syntax or semantics between releases.
Where destructuring remains safe
Destructuring is still perfectly fine where position is inherent and stable:
// Maps: key/value is a stable two-element contract
for ((key, value) in map) { ... }
// Pair/Triple: positions are the API
val (first, second) = Pair("a", "b")
The risk is specifically with data classes whose property order is an implementation detail, not a semantic contract.
What to do about it
Audit your destructuring sites. Search your codebase for val ( patterns on data classes with same-typed properties. These are your highest-risk locations. Replace them with explicit property access until name-based destructuring is stable.
Add a lint rule. Configure detekt or a custom lint check to warn on positional destructuring of data classes where adjacent properties share a type. This is cheap insurance.
Track the feature, but don’t ship it yet. Play with name-based destructuring in side projects or experiment branches to learn the syntax. Watch the Kotlin KEEP and release notes for stabilization. When it lands as stable, migrate. You’ll wipe out a whole class of silent bugs in one move.