skip to content

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?

level: middleimportance: must knowfreq 65%

answer

  1. Strict by default -> unknown key throws
  2. Json { ignoreUnknownKeys = true } = forward compat
  3. @JsonIgnoreUnknownKeys for per-class opt-in
  4. Tradeoff: typos/renames silently swallowed
  5. Pair with defaults for two-way evolution

basics

~10 s

Configure 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 s

Build 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 lines
kotlin
val 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 skipped

go deeper

for a junior

Knows the flag name and that it skips extra keys.

for a middle

Explains it gives forward compatibility and names the strictness tradeoff.

for a senior

Chooses global vs @JsonIgnoreUnknownKeys per boundary and pairs it with defaults for two-way evolution.

for a principal

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

context