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 scope
  • Class 'Y' is not registered for polymorphic serialization
  • Encountered 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. 1.Configure sealed class with proper annotations:
  2. 2.```kotlin
  3. 3.@Serializable
  4. 4.sealed class User {
  5. 5.abstract val id: String
  6. 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. 1.Configure custom class discriminator:
  2. 2.```kotlin
  3. 3.val json = Json {
  4. 4.classDiscriminator = "user_type" // Instead of default "type"
  5. 5.serializersModule = SerializersModule {
  6. 6.polymorphic(User::class) {
  7. 7.subclass(BasicUser::class, BasicUser.serializer())
  8. 8.subclass(PremiumUser::class, PremiumUser.serializer())
  9. 9.}
  10. 10.}
  11. 11.}

// Now expects: {"user_type":"premium","id":"123",...} ```

  1. 1.Handle missing discriminator with content-based detection:
  2. 2.```kotlin
  3. 3.@Serializable
  4. 4.data class RawUser(
  5. 5.val id: String,
  6. 6.val tier: String? = null,
  7. 7.val subscriptionEnds: String? = null
  8. 8.) {
  9. 9.fun toUser(): User = when {
  10. 10.subscriptionEnds != null -> PremiumUser(id, subscriptionEnds)
  11. 11.tier != null -> BasicUser(id, tier)
  12. 12.else -> GuestUser(id)
  13. 13.}
  14. 14.}

// Parse without discriminator val raw = Json.decodeFromString<RawUser>(jsonString) val user = raw.toUser() ```

  1. 1.Use sealed interface with Kotlin 1.5+:
  2. 2.```kotlin
  3. 3.@Serializable
  4. 4.sealed interface ApiResponse<out T> {
  5. 5.@Serializable
  6. 6.@SerialName("success")
  7. 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. 1.Register all subclasses in the module:
  2. 2.```kotlin
  3. 3.val json = Json {
  4. 4.serializersModule = SerializersModule {
  5. 5.contextual(User::class) { type ->
  6. 6.polymorphic(User::class, User.serializer(), {
  7. 7.subclass(BasicUser::class)
  8. 8.subclass(PremiumUser::class)
  9. 9.subclass(GuestUser::class)
  10. 10.// Must register ALL subclasses
  11. 11.})
  12. 12.}
  13. 13.}
  14. 14.}
  15. 15.`

Prevention

  • Always annotate sealed class and ALL subclasses with @Serializable
  • Use @SerialName to control the discriminator value explicitly
  • Match the classDiscriminator to the API's actual type field name
  • Add unit tests for each subclass serialization and deserialization
  • Use Json.encodeToString to verify the expected JSON format
  • Document the expected JSON format for each polymorphic type