Introduction
kotlinx.serialization supports polymorphic serialization through a class discriminator field that identifies the concrete subclass in JSON. When this discriminator is missing, has an unexpected value, or the subclass is not registered, deserialization fails with a clear but sometimes confusing error about unknown or missing serializers.
Symptoms
Serializer for subclass 'X' is not found in the polymorphic scopeClass 'Y' is not registered for polymorphic serializationEncountered an unknown serialization name 'UnknownType'- Sealed class deserialization fails for valid JSON
- Works for some subclasses but not others
Example error:
``
kotlinx.serialization.SerializationException:
Serializer for subclass 'PremiumUser' is not found in the polymorphic scope
of class 'User'. Registered subclasses: [BasicUser, GuestUser]
Common Causes
- Subclass not annotated with
@Serializable - Subclass not registered in
SerializersModule - API uses different discriminator field name than configured
- API does not include discriminator in JSON
- Sealed class hierarchy changed without updating serialization
Step-by-Step Fix
- 1.Configure sealed class with proper annotations:
- 2.```kotlin
- 3.@Serializable
- 4.sealed class User {
- 5.abstract val id: String
- 6.}
@Serializable @SerialName("basic") data class BasicUser( override val id: String, val tier: String ) : User()
@Serializable @SerialName("premium") data class PremiumUser( override val id: String, val subscriptionEnds: String ) : User()
// JSON must include the type field: // {"type":"premium","id":"123","subscriptionEnds":"2027-01-01"} ```
- 1.Configure custom class discriminator:
- 2.```kotlin
- 3.val json = Json {
- 4.classDiscriminator = "user_type" // Instead of default "type"
- 5.serializersModule = SerializersModule {
- 6.polymorphic(User::class) {
- 7.subclass(BasicUser::class, BasicUser.serializer())
- 8.subclass(PremiumUser::class, PremiumUser.serializer())
- 9.}
- 10.}
- 11.}
// Now expects: {"user_type":"premium","id":"123",...} ```
- 1.Handle missing discriminator with content-based detection:
- 2.```kotlin
- 3.@Serializable
- 4.data class RawUser(
- 5.val id: String,
- 6.val tier: String? = null,
- 7.val subscriptionEnds: String? = null
- 8.) {
- 9.fun toUser(): User = when {
- 10.subscriptionEnds != null -> PremiumUser(id, subscriptionEnds)
- 11.tier != null -> BasicUser(id, tier)
- 12.else -> GuestUser(id)
- 13.}
- 14.}
// Parse without discriminator val raw = Json.decodeFromString<RawUser>(jsonString) val user = raw.toUser() ```
- 1.Use sealed interface with Kotlin 1.5+:
- 2.```kotlin
- 3.@Serializable
- 4.sealed interface ApiResponse<out T> {
- 5.@Serializable
- 6.@SerialName("success")
- 7.data class Success<T>(val data: T) : ApiResponse<T>
@Serializable @SerialName("error") data class Error(val message: String, val code: Int) : ApiResponse<Nothing> }
// JSON: {"type":"success","data":{"id":"123"}} // or: {"type":"error","message":"Not found","code":404} ```
- 1.Register all subclasses in the module:
- 2.```kotlin
- 3.val json = Json {
- 4.serializersModule = SerializersModule {
- 5.contextual(User::class) { type ->
- 6.polymorphic(User::class, User.serializer(), {
- 7.subclass(BasicUser::class)
- 8.subclass(PremiumUser::class)
- 9.subclass(GuestUser::class)
- 10.// Must register ALL subclasses
- 11.})
- 12.}
- 13.}
- 14.}
- 15.
`
Prevention
- Always annotate sealed class and ALL subclasses with
@Serializable - Use
@SerialNameto control the discriminator value explicitly - Match the
classDiscriminatorto the API's actual type field name - Add unit tests for each subclass serialization and deserialization
- Use
Json.encodeToStringto verify the expected JSON format - Document the expected JSON format for each polymorphic type