You ship a sealed @Serializable hierarchy as a public JSON API. A teammate renames a subclass or refactors its package. Why might that break existing clients, and how do you prevent it?
answer
- no @SerialName => discriminator = FQN
- rename/move package => silent wire break
- @SerialName makes the value a stable contract
- ignoreUnknownKeys is for fields, not discriminator values
- defaultDeserializer handles unknown subtypes
basics
~20 sBy default the discriminator value is the class's full name. Renaming the class or moving its package changes that value, so old JSON no longer matches. Pin a stable value with @SerialName on every subclass.
solid answer
~40 sWithout @SerialName, a subclass's serial name defaults to its fully-qualified class name, and that string is written as the discriminator value (default key "type"). If a teammate renames the class or changes its package, the serial name silently changes, so previously stored or in-flight JSON now carries a discriminator the new code doesn't recognize — decoding throws SerializationException. The fix is to put an explicit, stable @SerialName("...") on every subclass so the wire value is decoupled from the Kotlin identifier. Treat those names as part of your public contract. For decode resilience against genuinely unknown future types, add a defaultDeserializer fallback in a polymorphic block (or for open hierarchies). Tests asserting exact JSON catch accidental drift.
go deeper
Recognizes that the class name appears in JSON and @SerialName can rename it.
Explains that FQN-as-discriminator makes refactors breaking and pins @SerialName as the contract.
Adds decode resilience (defaultDeserializer) and contract tests; distinguishes unknown fields vs unknown subtypes.
Establishes wire-contract governance: versioned serial-name registry, compatibility tests, and rollout policy across producers/consumers.
## Where the discriminator value comes from The polymorphic discriminator **value** is the subclass's **serial name**. If you don't annotate it, the serial name **defaults to the fully-qualified class name** (e.g. `com.acme.api.Event.Click`). That string is embedded in every serialized payload. ## The failure Because the FQN leaks into the wire format, ordinary refactors become **breaking API changes**: - Rename `Click` -> `Tap`: discriminator changes from `...Event.Click` to `...Event.Tap`. - Move package `api` -> `events`: prefix changes. - Convert nested -> top-level class: name changes. Old persisted documents and clients still send the old value; new code has no subtype registered under it, so decode throws `SerializationException`. ## The fix: pin @SerialName ```kotlin @Serializable sealed class Event { @Serializable @SerialName("click") // stable wire value data class Click(val x: Int, val y: Int) : Event() @Serializable @SerialName("key_press") data class Key(val code: Int) : Event() } ``` Now Kotlin names can change freely; the `@SerialName` strings are the **contract**. Choose short, stable, lowercase tokens, not class names. ## Defensive decoding for the future When new subtypes may appear that **old** clients can't know, register a fallback so decoding doesn't crash on unknown discriminators: ```kotlin val module = SerializersModule { polymorphic(Event::class) { defaultDeserializer { UnknownEvent.serializer() } } } ``` (`Json { ignoreUnknownKeys = true }` handles unknown *fields*, but NOT unknown discriminator *values* — that's what `defaultDeserializer` is for.) ## Guardrails - Unit tests that assert the **exact** JSON string for each subtype catch accidental serial-name drift in review. - Document the @SerialName tokens as the versioned contract. ## Key APIs/keywords `@SerialName`, `classDiscriminator`, `SerializationException`, `defaultDeserializer`, `ignoreUnknownKeys`, `PolymorphicSerializer`.
- Does Json { ignoreUnknownKeys = true } help when an unknown subtype's discriminator arrives?No. It only ignores unknown object fields. An unknown discriminator value still throws unless you set a defaultDeserializer fallback.
- How do you catch serial-name drift before release?Write tests that assert the exact encoded JSON for each subtype; a rename then fails the test in review.
Defaulting the discriminator to the class name is like printing your home address on every product — move house and all the returns get lost.
saying these in an interview costs you the question
- Believing class renames are safe for serialized data without @SerialName
- Confusing ignoreUnknownKeys (fields) with discriminator handling (subtypes)
- Treating the FQN discriminator as 'just an implementation detail'
- No tests pinning the on-wire JSON shape