You have a sealed @Serializable Result<T> hierarchy and you also nest one sealed type as a property of another. What subtleties arise with discriminator collisions, generic type arguments, and decoding the base vs a concrete subtype?
answer
- encode through BASE type or no discriminator appears
- generic: supply element serializer; T keeps its own serializer
- nested sealed property => independent discriminator
- discriminator key must not collide with a property name
- @JsonClassDiscriminator scopes the key per hierarchy
basics
~20 sPolymorphic encoding only happens when you serialize through the base type, not a concrete subtype. Generic type arguments still need their own serializers, and a nested sealed property must itself be polymorphically encoded. The discriminator key must not collide with a real field name.
solid answer
~40 sThree subtleties. First, dispatch depends on the static type you serialize through: encodeToString<Base>(value) emits the discriminator; encodeToString(concreteSubtype) without the base type often serializes it as just that class, with no tag — breaking round-trips. Use the base serializer or PolymorphicSerializer explicitly. Second, generics: a sealed Result<T> needs a serializer for T; the plugin generates Result.serializer(elementSerializer), and the discriminator tags the Result subtype while T is serialized by its own serializer — T itself can be polymorphic if it's a registered base. Third, a sealed type used as a property is auto-polymorphic, so the discriminator key (default "type") must not collide with any actual property name in any subtype, or you get a conflict exception; rename via classDiscriminator. Nested sealed types each carry their own discriminator independently.
code
kotlin · 8 lines@Serializable
@JsonClassDiscriminator("kind") // scope discriminator key to avoid collision
sealed class Node {
@Serializable @SerialName("leaf") data class Leaf(val v: Int) : Node()
@Serializable @SerialName("branch") data class Branch(val left: Node, val right: Node) : Node()
}
// Branch nests Node fields, each with their own "kind" tag.
val s = Json.encodeToString<Node>(Node.Branch(Node.Leaf(1), Node.Leaf(2)))go deeper
Understands you decode/encode through the base type to get polymorphism.
Knows nested sealed fields are polymorphic and the discriminator key can collide with a property.
Handles generic sealed serializers, static-type dispatch pitfalls, and per-hierarchy @JsonClassDiscriminator.
Designs robust generic/nested wire schemas, prevents collisions by convention, and codifies base-type encoding to avoid silent round-trip breakage.
## 1. Dispatch is driven by the STATIC type you encode through Polymorphic discriminator injection happens only when the **declared/passed serializer is the base type's polymorphic serializer**. ```kotlin @Serializable sealed class Event { @Serializable @SerialName("click") data class Click(val x: Int): Event() } val e: Event = Event.Click(1) Json.encodeToString(e) // type known is Event -> {"type":"click","x":1} Json.encodeToString(Event.Click(1)) // static type Click -> {"x":1} NO discriminator! ``` If you encode the **concrete** type, you get the bare subtype with **no tag**, so decoding back through `Event` fails. Always encode through the **base** (`val e: Event = ...` or `Json.encodeToString<Event>(...)` / `Event.serializer()` / `PolymorphicSerializer(Event::class)`). ## 2. Generic sealed hierarchies ```kotlin @Serializable sealed class Result<out T> { @Serializable @SerialName("ok") data class Ok<T>(val value: T) : Result<T>() @Serializable @SerialName("err") data class Err(val message: String) : Result<Nothing>() } val s = Json.encodeToString(Result.serializer(Int.serializer()), Result.Ok(5)) // {"type":"ok","value":5} ``` - The plugin generates a **parameterized** `Result.serializer(tSerializer)`. You must supply the element serializer (the `reified` `encodeToString<Result<Int>>(...)` overload does this for you via `serializer()`). - The **discriminator tags only the Result subtype**; the payload `T` is serialized by **its own** serializer. If `T` is itself a polymorphic base, its value gets its own nested discriminator. ## 3. Nesting a sealed type as a property A sealed/polymorphic type used as a **field** is encoded polymorphically too: ```kotlin @Serializable data class Envelope(val payload: Event) // {"payload":{"type":"click","x":1}} ``` Each nested polymorphic value carries its **own** discriminator independently. Multiple levels nest cleanly. ## 4. Discriminator/field-name collisions The discriminator key (default `"type"`) is injected into the subtype object. If **any** subtype already has a real property named `type`, you get a **conflict** at init/encode time: ``` IllegalStateException/SerializationException: discriminator 'type' collides with a property ``` Fix: rename the property, or set a non-colliding key with `Json { classDiscriminator = "_t" }`, or use `@JsonClassDiscriminator("...")` on the base to scope it per-hierarchy. ## 5. Decoding base vs concrete - Decode through the **base** (`decodeFromString<Event>`) to use the discriminator and pick the subtype. - Decode through a **concrete** type only if the JSON has **no** discriminator (i.e., was encoded as that concrete type). Mixing the two is the classic round-trip bug. ## Key APIs/keywords `PolymorphicSerializer`, `Result.serializer(elementSerializer)`, `@SerialName`, `classDiscriminator`, `@JsonClassDiscriminator`, `reified serializer()`, `SerializationException`.
- Why does encodeToString(concreteSubtype) sometimes omit the discriminator?Because the static type is the concrete subtype, not the polymorphic base, so the polymorphic serializer that injects the tag isn't selected.
- A subtype has a field named 'type'. What happens and how do you fix it?It collides with the default discriminator key and throws. Rename the field or set classDiscriminator/@JsonClassDiscriminator to a non-colliding key.
Encoding a concrete subtype without the base is like mailing a parcel with the contents but no shipping label — the receiver can't tell what kind of box to open it as.
saying these in an interview costs you the question
- Encoding the concrete subtype and expecting a discriminator
- Forgetting to provide the element serializer for a generic sealed type
- Naming a real property the same as the discriminator key
- Assuming nested polymorphic fields don't each get their own tag