skip to content

How do Json configuration flags like ignoreUnknownKeys, coerceInputValues, isLenient, and decodeEnumsCaseInsensitive affect the security/trust posture of deserializing untrusted input?

level: seniorimportance: nice to knowfreq 30%

answer

  1. Flags tune leniency, not gadget surface
  2. ignoreUnknownKeys hides extra fields (default false)
  3. coerceInputValues silently swaps invalid → default
  4. isLenient widens grammar; case-insensitive enums widen match
  5. Untrusted = fail loud (strict); still validate in init {}

basics

~10 s

These flags make the parser more forgiving. Forgiving is convenient but can silently accept or change attacker data. For untrusted input, prefer strict settings so unexpected data fails loudly instead of slipping through.

solid answer

~50 s

Each leniency flag trades strictness for convenience and shifts the trust posture. `ignoreUnknownKeys` (default false) silently drops unexpected properties — convenient for schema evolution but it hides extra attacker-supplied fields; keep it false for strict contracts. `coerceInputValues` (default false) replaces invalid enum values / explicit nulls on non-null defaulted fields with the *default* instead of throwing — that means hostile input can be coerced to a 'valid-looking' default, so you may accept data you'd rather reject. `isLenient` relaxes JSON syntax (unquoted keys, etc.), enlarging the accepted grammar and weakening canonicalization. `decodeEnumsCaseInsensitive` widens enum matching. None of these create a gadget-RCE primitive, but several can cause *silent acceptance/transformation* of malicious or malformed data, which is a logic/validation risk. For untrusted boundaries default to strict (all false) so anomalies surface as `SerializationException`, and still validate values in `init {}`.

code

kotlin · 9 lines
kotlin
// Risky for untrusted input: invalid enum coerced to default, extras dropped
val lax = Json { coerceInputValues = true; ignoreUnknownKeys = true }

enum class Role { USER, ADMIN }
@Serializable data class Req(val role: Role = Role.USER)

// {"role":"SUPERADMIN"} -> with coerceInputValues=true, role becomes USER
// silently, instead of throwing SerializationException.
val r = lax.decodeFromString<Req>("{\"role\":\"SUPERADMIN\"}")

go deeper

for a junior

Knows these flags make parsing more forgiving and that strict is safer for untrusted input; may not detail each flag.

for a middle

Explains ignoreUnknownKeys and coerceInputValues behavior and why strict defaults help.

for a senior

Articulates the silent-accept/transform risk per flag, separates it from gadget surface, and prescribes strict-for-untrusted + deliberate evolution exceptions.

for a principal

Owns the org-wide policy for strictness vs compatibility, codifies it in shared Json instances, and balances evolution needs against security/audit requirements.

## These flags tune the *accepting grammar*, not the gadget surface None of the `Json {}` leniency flags reintroduce arbitrary-class instantiation — that property is fixed by codegen. What they change is **how forgiving** decoding is, which affects whether hostile/malformed data is *rejected loudly* or *silently accepted/altered*. For untrusted input, silent acceptance is the risk. ```kotlin val strict = Json { // recommended for untrusted boundaries ignoreUnknownKeys = false // default coerceInputValues = false // default isLenient = false // default decodeEnumsCaseInsensitive = false // default } ``` ## Flag-by-flag - **`ignoreUnknownKeys`** (default `false`): when `true`, properties not in the `SerialDescriptor` are *dropped silently*. Useful for forward-compatible schema evolution, but it hides that the caller sent unexpected fields (possible probing or contract drift). Strict contracts should leave it `false` so unknown keys throw `SerializationException`. - **`coerceInputValues`** (default `false`): when `true`, an *invalid enum constant* or an *explicit null for a non-null property that has a default* is replaced by the **default value** instead of failing. Security angle: attacker input that should be rejected gets quietly normalized to a default — you might persist or act on a value the user never legitimately sent. Prefer `false` so invalid values throw. - **`isLenient`** (default `false`): relaxes strict JSON (unquoted string literals/keys, etc.). Expands the accepted grammar and undermines canonical parsing; avoid for untrusted input. - **`decodeEnumsCaseInsensitive`** (default `false`): matches enum names ignoring case. Convenience that widens the match set; usually keep strict. - Related: **`explicitNulls`** (default `true`) controls whether nulls are emitted/required; **`allowStructuredMapKeys`**, **`allowSpecialFloatingPointValues`** also broaden inputs (e.g. `NaN`, `Infinity`) — broaden only with reason. ## The principle: fail loud at trust boundaries For data you trust (internal config you author) leniency may be fine. For **untrusted** input, you want anomalies to **fail closed** so they hit your error handling and validation rather than slipping in as silent defaults. So: strict `Json` config + invariant checks in `init {}` + catching `SerializationException`/`IllegalArgumentException`. ## Schema evolution caveat The one flag people legitimately flip is `ignoreUnknownKeys = true` to allow new producer fields without breaking old consumers. That is a deliberate evolution decision — pair it with the knowledge that you are intentionally ignoring data, and never let it lull you into skipping value validation. ## Summary These flags move the strictness dial, not the gadget surface. Untrusted input → keep them strict so bad data throws; reserve leniency (especially `ignoreUnknownKeys`) for deliberate compatibility choices, and validate values regardless.

  • Give a concrete security downside of coerceInputValues = true.
    An invalid/forbidden enum or an explicit null can be silently replaced by the field's default, so input you intended to reject is accepted as a default value — masking malformed or probing requests.
  • When is ignoreUnknownKeys = true justified?
    Deliberate schema evolution: letting newer producers add fields without breaking older consumers. It is a compatibility choice, not a security relaxation, and never replaces value validation.

Leniency flags are like turning off the spellchecker's red underline: errors don't disappear, they just stop being flagged.

saying these in an interview costs you the question

  • Claiming these flags affect gadget/RCE risk
  • Enabling coerceInputValues on untrusted input without realizing invalid values become defaults
  • Turning on isLenient for external/untrusted JSON for convenience
  • Using ignoreUnknownKeys = true and then assuming all received data was validated
  • Treating leniency as a substitute for init {} validation

context