skip to content

kotlinx.serialization supports more than one way of laying out polymorphic data on the wire. Explain ARRAY_WRAPPED vs the discriminator object mode, and when you must use which.

level: seniorimportance: should knowfreq 35%

answer

  1. object mode injects 'type' field; needs an object payload
  2. ARRAY_WRAPPED = [typeName, payload]
  3. non-object subtype (value class/primitive) forces array mode
  4. useArrayPolymorphism = true / classDiscriminatorMode
  5. object mode = REST-friendly default

basics

~20 s

Normally the type name is a field inside the object (discriminator mode). But that only works when the value is a JSON object. For non-object values (like a primitive), the library wraps it as a two-element array [typeName, value]. You can also force array mode.

solid answer

~40 s

The default JSON polymorphism is JsonClassDiscriminator mode: an extra field (default "type") is injected into the serialized **object**. This requires the subtype to serialize to a JSON **object** — you can't inject a key into a JSON number or array. To handle non-object or structurally conflicting cases, kotlinx.serialization can use ARRAY_WRAPPED mode, emitting a two-element array: [discriminatorValue, payload]. You opt in via Json { classDiscriminatorMode = ... } or by setting useArrayPolymorphism = true (legacy flag). Array mode is mandatory when a subtype's serializer produces a non-object (e.g., a value class serializing to a primitive) or when the payload could itself contain a key colliding with the discriminator. The trade-off: array mode is less human-readable and not idiomatic REST JSON, so prefer the object/discriminator mode for public APIs unless a subtype forces array mode.

go deeper

for a junior

Aware that the type tag normally lives inside the JSON object.

for a middle

Can switch the discriminator key and knows array mode exists as an alternative layout.

for a senior

Explains exactly when array mode is mandatory (non-object subtypes, key collisions) and the readability trade-off.

for a principal

Sets format policy across services: object mode for public APIs, array mode only where forced, with versioned discriminator governance.

## Two on-wire layouts kotlinx.serialization can encode a polymorphic value two ways: ### 1. Discriminator-in-object (default) The type tag is an **extra field** inside the object: ```json { "type": "click", "x": 3, "y": 4 } ``` Controlled by `JsonClassDiscriminator` / `Json { classDiscriminator = "type" }`. This is clean, REST-friendly, and the usual choice. ### 2. ARRAY_WRAPPED The value is encoded as a **two-element JSON array** `[discriminator, payload]`: ```json [ "click", { "x": 3, "y": 4 } ] ``` Enabled by `Json { useArrayPolymorphism = true }` (legacy boolean) or by the broader `classDiscriminatorMode` setting. ## Why array mode exists — when you MUST use it The object/discriminator mode **injects a key into the payload object**. That's impossible (or unsafe) when: - **The subtype doesn't serialize to an object.** A value/inline class or a custom serializer can serialize to a JSON **primitive** or **array**. You can't add a `"type"` field to `42` or to `[1,2,3]`. Array wrapping is then the only correct layout. - **Key collision risk.** If the payload itself contains a field literally named like the discriminator, injecting the discriminator would clash. Object mode actually throws if the payload already has the discriminator key; array mode sidesteps this because the tag lives outside the payload. ## How to choose ```kotlin // Object / discriminator mode (default) - REST-friendly val rest = Json { classDiscriminator = "type" } // Array mode - required for non-object subtypes val arr = Json { useArrayPolymorphism = true } ``` - **Public HTTP/JSON APIs**: prefer object mode; it's idiomatic and readable. - **Subtypes that are value classes / primitive-serializing / custom non-object serializers**: array mode is required. - **Internal/storage formats where readability is irrelevant**: either; array mode avoids collisions. ## Discriminator-output controls `classDiscriminatorMode` also lets you suppress the discriminator entirely (e.g., `POLYMORPHIC` vs `ALL` vs `NONE`-style behavior depending on version) — useful when the type is implied by context and you don't want the tag emitted. Be careful: if you stop emitting a discriminator, decoding back to the base type can become ambiguous. ## Key APIs/keywords `classDiscriminator`, `useArrayPolymorphism`, `classDiscriminatorMode`, `JsonClassDiscriminator`, value/inline classes, custom serializers, `JsonObject` vs `JsonArray`.

  • What error can object/discriminator mode raise that array mode avoids?
    If the payload already contains a field with the discriminator key name, object mode conflicts/throws; array mode keeps the tag outside the payload.
  • Why can't a value class serializing to a primitive use object discriminator mode?
    There's no JSON object to inject a 'type' field into — the value is a bare number/string — so array wrapping is required.

saying these in an interview costs you the question

  • Claiming object discriminator mode works for any subtype including primitives
  • Not knowing array mode exists / why it's needed
  • Confusing classDiscriminator (the key string) with useArrayPolymorphism (the layout)
  • Recommending array mode for public REST APIs without reason

context