An old service deserializes JSON produced by a newer service that added extra fields. By default kotlinx.serialization throws. How do you handle the extra keys, and what is the tradeoff?
answer
- Strict by default -> unknown key throws
- Json { ignoreUnknownKeys = true } = forward compat
- @JsonIgnoreUnknownKeys for per-class opt-in
- Tradeoff: typos/renames silently swallowed
- Pair with defaults for two-way evolution
basics
~10 sConfigure the Json instance with ignoreUnknownKeys = true. Then unknown extra keys are skipped instead of causing an error, letting old code read newer data.
solid answer
~40 sBuild a configured `Json { ignoreUnknownKeys = true }` instance and reuse it. By default kotlinx.serialization is strict: an unknown key triggers `SerializationException`. Setting `ignoreUnknownKeys = true` makes the decoder skip keys it doesn't map to a property, giving you **forward compatibility** — an old reader tolerates fields a newer writer added. The tradeoff is loss of strictness: genuine typos or schema mistakes (e.g. `nme` instead of `name`) are silently ignored, so you trade safety for resilience. Best practice is to enable it on consumer boundaries that must accept evolving upstream data, but keep strict decoding for internal/validated inputs. A finer-grained alternative is the `@JsonIgnoreUnknownKeys` annotation (class-level) so you can opt in per type rather than globally. Pair it with defaults for the two-way evolution story.
code
kotlin · 7 linesval json = Json { ignoreUnknownKeys = true }
@Serializable data class Order(val id: Long, val total: Double)
// producer added 'currency' the consumer doesn't model yet
val payload = """{"id":7,"total":9.99,"currency":"EUR"}"""
json.decodeFromString<Order>(payload) // Order(7, 9.99); currency skippedgo deeper
Knows the flag name and that it skips extra keys.
Explains it gives forward compatibility and names the strictness tradeoff.
Chooses global vs @JsonIgnoreUnknownKeys per boundary and pairs it with defaults for two-way evolution.
Sets an org policy on which boundaries relax strictness and how it interacts with rolling deploys and contract validation.
## Strict by default kotlinx.serialization's `Json` is **strict**: if the input contains a key that doesn't correspond to any property in the target class, decoding fails with a `SerializationException`. This protects against malformed or unexpected input. ## Forward compatibility In an evolving system, a **newer producer** may add fields that an **older consumer** doesn't know about. To let the old consumer keep working, relax strictness: ```kotlin import kotlinx.serialization.json.Json val lenient = Json { ignoreUnknownKeys = true } @Serializable data class UserV1(val id: Long, val name: String) val newerJson = """{"id":1,"name":"Ada","role":"ADMIN","mfa":true}""" val u = lenient.decodeFromString<UserV1>(newerJson) // 'role' and 'mfa' are skipped; u = UserV1(1, "Ada") ``` ## Global vs per-class - **Global:** `Json { ignoreUnknownKeys = true }` applies to every type decoded by that instance. - **Per-class:** annotate the class with `@JsonIgnoreUnknownKeys` to opt in only where you expect upstream evolution, keeping strictness elsewhere. ## The tradeoff Ignoring unknown keys means **silently swallowing** anything unexpected. A real bug — a renamed or misspelled key in *your own* schema — no longer surfaces as an error. So: - Enable on **trust-but-evolve** boundaries (external/upstream services you don't control versioning of). - Keep **strict** on internal contracts where a surprise key signals a defect. ## Two-way evolution recap - `ignoreUnknownKeys` → old reader tolerates **new** fields (forward). - Default values → new reader tolerates **missing** fields (backward). Using both together gives smooth rolling deployments where producers and consumers update independently. ## Reuse the instance Create one configured `Json` and reuse it; constructing `Json {}` repeatedly is wasteful and can subtly diverge in config.
- How do you enable lenient unknown-key handling for only one class?Annotate that class with @JsonIgnoreUnknownKeys instead of setting the global flag.
- What's the downside of always enabling ignoreUnknownKeys?Misspelled or renamed keys in your own schema are silently ignored, hiding real bugs.
ignoreUnknownKeys is like a mail sorter that drops envelopes it has no slot for instead of halting the whole line.
saying these in an interview costs you the question
- Thinking kotlinx.serialization ignores unknown keys by default
- Confusing ignoreUnknownKeys (forward) with defaults (backward)
- Claiming it can recover the dropped data later
- Recommending it globally with no mention of the strictness tradeoff
- Constructing a new Json {} for every call