Why do sealed hierarchies serialize polymorphically without a SerializersModule, while interfaces and open classes require explicit registration?
answer
- Sealed = exhaustive = compile-time enumeration
- Open/interface = unbounded = runtime registration
- Same discriminator wire format either way
- Missing subclass() => runtime SerializationException
- Open allows default { } / defaultDeserializer { }
basics
~20 sA sealed type lists all its subtypes in one place, so the compiler already knows them and wires them up. Open classes and interfaces can be extended anywhere, so the compiler can't list them and you must register subtypes yourself.
solid answer
~40 sSealed classes/interfaces are exhaustive: every direct subtype must be declared in the same module, so the @Serializable plugin can enumerate them at compile time and generate a sealed polymorphic serializer that knows all branches — no SerializersModule needed. Open classes and plain interfaces have an unbounded subtype set; the compiler cannot close the list, so you register each subtype at runtime via SerializersModule { polymorphic(Base::class) { subclass(...) } }. Both produce the same discriminator-based wire format. A practical consequence: adding a new sealed subtype is a compile-time change everywhere, while open hierarchies let you register subtypes lazily or per-module — flexible but with no compile-time guarantee that all subtypes are covered, surfacing as a runtime SerializationException.
go deeper
Knows sealed works out of the box and open needs registration.
Explains exhaustiveness vs unbounded sets and that both share the discriminator format.
Discusses augmenting sealed serialization from other modules and default { } handling for open sets.
Weighs sealed (closed-domain safety) vs open (plugin extensibility) as an architecture decision across module boundaries.
## Exhaustiveness is the whole story `sealed` means *all direct subtypes are known to the compiler*. They must live in the same module (in modern Kotlin, the same module/package constraints for sealed). Because the set is closed, the `@Serializable` plugin generates a **sealed polymorphic serializer** that contains a `when` over every subtype — registration is automatic. ```kotlin @Serializable sealed interface Event @Serializable @SerialName("login") data class Login(val user: String) : Event @Serializable @SerialName("logout") data object Logout : Event // No SerializersModule needed: val json = Json val e: Event = Login("ann") json.encodeToString(e) // {"type":"login","user":"ann"} ``` ## Open / interface: the set is unbounded Anyone, in any module, can implement an interface or extend an `open class`. The compiler can't build a closed `when`, so there's nothing to generate automatically. You supply the mapping at runtime: ```kotlin val module = SerializersModule { polymorphic(PaymentMethod::class) { subclass(Card::class) subclass(Paypal::class) } } ``` ## Same wire format Both approaches emit the discriminator (`type` by default). The difference is purely *who builds the subtype table*: the compiler (sealed) vs. you (open). ## Trade-offs - **Sealed**: compile-time completeness; adding a subtype forces you to update exhaustive `when`s; ideal for closed domains. - **Open**: extensible across modules/plugins; you can register defaults via `default { }` / `defaultDeserializer { }`; risk is a missing `subclass(...)` only showing up at runtime as `SerializationException: Class '...' is not registered`. ## Hybrid You can even mix: register *extra* subtypes of a sealed base inside a `polymorphic` block to extend it from another module, since the module's table augments the generated one.
- Can you extend a sealed hierarchy's polymorphic serialization from another module?Yes — register additional concrete subtypes in a polymorphic { subclass } block; the runtime module augments the compiler-generated sealed table.
- What error indicates a missing open-hierarchy registration, and when does it appear?SerializationException 'Class X is not registered for polymorphic serialization', thrown at runtime during encode/decode — never at compile time.
Sealed is a guest list printed by the host (fixed, known); open is an open invitation where you must personally vouch for each arrival at the door.
saying these in an interview costs you the question
- Claiming sealed classes need manual SerializersModule registration
- Thinking open and sealed produce different wire formats
- Believing the compiler can enumerate interface implementors
- Not recognizing the runtime SerializationException as a missing registration
- Saying sealed subtypes can live in any module without constraints