skip to content

How do you register an open class hierarchy for polymorphic serialization using the polymorphic { subclass() } builder, and what JSON does it produce?

level: middleimportance: must knowfreq 50%

answer

  1. polymorphic(Base::class) { subclass(Impl::class) }
  2. Discriminator key default = "type"
  3. Value = @SerialName or FQ class name
  4. Must encode through the base static type
  5. classDiscriminator / useArrayPolymorphism config

basics

~10 s

Inside SerializersModule { } you call polymorphic(Base::class) { subclass(Impl::class) } for each implementation. The output JSON gets an extra type field naming which subclass it is, so decoding can pick the right one.

solid answer

~40 s

Use the polymorphic builder: SerializersModule { polymorphic(Base::class) { subclass(A::class); subclass(B::class) } }. Each subclass must be @Serializable. At encode time the Json format writes a class discriminator (default key "type") whose value is the subtype's @SerialName (or fully-qualified class name if none). The reference must be typed as the base — encoding a concrete A directly skips polymorphism. Decoding reads the discriminator and dispatches to the registered serializer. You can configure the discriminator key via Json { classDiscriminator = "_type" } and switch shapes with JsonBuilder.useArrayPolymorphism or classDiscriminatorMode. Unregistered subtypes throw SerializationException at runtime; sealed hierarchies don't need this since they register automatically.

code

kotlin · 16 lines
kotlin
interface Shape
@Serializable @SerialName("circle") data class Circle(val r: Double) : Shape
@Serializable @SerialName("rect") data class Rect(val w: Double, val h: Double) : Shape

val json = Json {
    serializersModule = SerializersModule {
        polymorphic(Shape::class) {
            subclass(Circle::class)
            subclass(Rect::class)
        }
    }
    classDiscriminator = "kind"
}

val s: Shape = Circle(2.0)
println(json.encodeToString(s)) // {"kind":"circle","r":2.0}

go deeper

for a junior

Can write a polymorphic { subclass } block and knows JSON gets a type field.

for a middle

Explains discriminator source (@SerialName), static-type requirement, and configuring classDiscriminator.

for a senior

Knows array polymorphism, classDiscriminatorMode, custom subclass serializers, and discriminator collisions.

for a principal

Designs stable discriminator conventions for schema evolution and cross-service compatibility.

## The builder `polymorphic(Base::class) { ... }` opens a scope where you enumerate concrete subtypes: ```kotlin @Serializable sealed interface Project // or open class / interface @Serializable @SerialName("owned") data class Owned(val owner: String) : Message @Serializable @SerialName("text") data class Text(val body: String) : Message interface Message val module = SerializersModule { polymorphic(Message::class) { subclass(Owned::class) subclass(Text::class) } } val json = Json { serializersModule = module } ``` Each `subclass(X::class)` requires `X` to be `@Serializable`. There's also `subclass(X::class, customSerializer)` to override the generated serializer, and `defaultDeserializer { ... }` / `default { ... }` for handling unknown discriminator values. ## The wire format With the default `Json`, a polymorphic value is encoded as an object with an extra **class discriminator** key, default `"type"`: ```json { "type": "text", "body": "hi" } ``` The value of `type` is the subtype's `@SerialName` if present, otherwise its fully qualified class name. Configure the key with `Json { classDiscriminator = "kind" }`. ## You must encode through the base type Polymorphism only kicks in when the static type is the base: ```kotlin val m: Message = Text("hi") json.encodeToString(m) // includes discriminator json.encodeToString(Text("hi")) // NO discriminator — concrete type ``` ## Alternative shapes - `Json { classDiscriminatorMode = ClassDiscriminatorMode.POLYMORPHIC }` controls when the discriminator appears. - `useArrayPolymorphism = true` emits `["text", { "body": "hi" }]` instead of an inline key — useful for formats without free-form keys. ## Failure modes An unregistered subtype, or a base with no `polymorphic` block, throws `SerializationException` at runtime. The discriminator key must not collide with a real property name (that also throws).

  • Why does encodeToString(Text("hi")) omit the discriminator while encodeToString(m: Message) includes it?
    The serializer is chosen from the static (declared) type. A concrete type uses its own object serializer; the base type uses the polymorphic serializer that writes the discriminator.
  • What happens if a subtype's property is also named "type"?
    It collides with the default class discriminator and Json throws an IllegalArgumentException; change classDiscriminator or rename the property.

saying these in an interview costs you the question

  • Forgetting that subclasses must each be @Serializable
  • Encoding the concrete type and expecting a discriminator
  • Not knowing the discriminator value comes from @SerialName / class name
  • Assuming you can register a non-@Serializable subclass without a custom serializer
  • Thinking the discriminator key is always 'type' and unchangeable

context