skip to content

What is the classDiscriminator setting in Json { }, and how does it relate to polymorphic serialization? When would you change it?

level: seniorimportance: should knowfreq 40%

answer

  1. classDiscriminator key default = "type"
  2. value = @SerialName (else FQN)
  3. change it to dodge a real "type" field
  4. @JsonClassDiscriminator per hierarchy
  5. classDiscriminatorMode: POLYMORPHIC/ALL/NONE

basics

~20 s

classDiscriminator is the JSON key (default "type") that records which concrete subclass a polymorphic object is, so the decoder can pick the right type. You change it to avoid clashing with a real "type" field.

solid answer

~40 s

When serializing a sealed/polymorphic hierarchy, kotlinx.serialization writes an extra key naming the concrete subtype so decoding can reconstruct the right class. classDiscriminator (default "type") sets that key's name globally on the Json instance. The value defaults to the fully-qualified serial name, overridable per class with @SerialName. You change classDiscriminator when your domain already has a real property named "type" that would collide. For finer control, classDiscriminatorMode controls when the discriminator is emitted (e.g. POLYMORPHIC vs ALL_JSON_OBJECTS vs NONE), and @JsonClassDiscriminator annotates a specific hierarchy with its own key. The discriminator only applies to JSON-object output; sealed hierarchies serialized as arrays or via custom serializers behave differently. Mismatched discriminator keys between producer and consumer cause decode failures, so it's part of the wire contract.

code

kotlin · 12 lines
kotlin
import kotlinx.serialization.*
import kotlinx.serialization.json.*

@Serializable sealed interface Shape
@Serializable @SerialName("circle") data class Circle(val r: Double) : Shape
@Serializable @SerialName("square") data class Square(val s: Double) : Shape

fun main() {
    val json = Json { classDiscriminator = "#kind" }
    println(json.encodeToString<Shape>(Circle(1.0))) // {"#kind":"circle","r":1.0}
    println(json.decodeFromString<Shape>("{\"#kind\":\"square\",\"s\":2.0}")) // Square(s=2.0)
}

go deeper

for a junior

Knows polymorphic JSON has a 'type' field identifying the subclass.

for a middle

Explains default key 'type', that the value is the serial name, and changing classDiscriminator to avoid collisions.

for a senior

Adds @JsonClassDiscriminator, classDiscriminatorMode, and pinning @SerialName for stable contracts.

for a principal

Treats the discriminator as a versioned wire contract; reasons about producer/consumer agreement, refactor safety, and content-based vs key-based polymorphism.

## Why a discriminator exists When you serialize a value through a **polymorphic** base type (a `sealed class`/`sealed interface`, or a registered open polymorphic type), the JSON must record *which concrete subtype* it is so the decoder can rebuild the correct class. kotlinx.serialization does this by adding a **class discriminator** key to the JSON object. ```kotlin @Serializable sealed interface Shape @Serializable @SerialName("circle") data class Circle(val r: Double) : Shape @Serializable @SerialName("square") data class Square(val s: Double) : Shape Json.encodeToString<Shape>(Circle(1.0)) // {"type":"circle","r":1.0} ``` - The **key** is `classDiscriminator` (default `"type"`). - The **value** is the type's serial name — by default the fully-qualified class name, overridable with `@SerialName`. ## Changing the key: classDiscriminator ```kotlin val json = Json { classDiscriminator = "#kind" } json.encodeToString<Shape>(Circle(1.0)) // {"#kind":"circle","r":1.0} ``` **When to change it:** your model already has a real property called `type`, which would collide with the default discriminator; renaming the discriminator avoids the clash. It's a global setting on the Json instance. ## Finer-grained control - **`@JsonClassDiscriminator("...")`** — annotate a specific sealed hierarchy to give it its own discriminator key, independent of the global setting. - **`classDiscriminatorMode`** — an enum controlling *when* the discriminator is written: `POLYMORPHIC` (default; only for polymorphic types), `ALL_JSON_OBJECTS`, or `NONE` (omit it — useful when the consumer doesn't expect one, but then you can't decode polymorphically by key). ## Contract implications The discriminator key and the serial-name values are part of the **wire contract**. Producer and consumer must agree on the key name and on each subtype's name; a mismatch causes a `SerializationException` on decode (unknown discriminator value or missing key). For this reason pin serial names with `@SerialName` rather than relying on package-qualified class names that change when you refactor. ## Scope The discriminator applies to JSON-**object** representations of polymorphic types. Other strategies (custom serializers, array-based encodings, or `JsonContentPolymorphicSerializer` which discriminates by content instead of a key) don't use this key.

  • How do you pin the discriminator value so refactoring a class doesn't break the wire format?
    Annotate each subtype with @SerialName("..."); the discriminator value uses that instead of the package-qualified class name.
  • What if a hierarchy needs a different discriminator key than the global one?
    Use @JsonClassDiscriminator("key") on that sealed base; it overrides the Json-level classDiscriminator for that hierarchy.

saying these in an interview costs you the question

  • Thinking the discriminator appears for non-polymorphic types by default
  • Assuming the value is always the simple class name (it's the serial name, default FQN)
  • Not treating the discriminator key/value as part of the wire contract
  • Confusing classDiscriminator (key name) with classDiscriminatorMode (when it's emitted)

context