Polymorphism

Slumber auto-detects sealed classes, adds type discriminators, and supports custom identifiers, default fallback types, and migration aliases.

Sealed classes — zero config

Sealed classes just work. Slumber adds a _type discriminator automatically:

sealed class Shape {
    data class Circle(val radius: Double) : Shape()
    data class Rectangle(val width: Double, val height: Double) : Shape()
}

val codec = Codec.default

// Slumber adds "_type" with the fully qualified class name
codec.slumber(Shape.Circle(5.0))
// → { "_type": "com.example.Shape.Circle", "radius": 5.0 }

// Awake reads "_type" and instantiates the correct subclass
codec.awake<Shape>(mapOf(
    "_type" to "com.example.Shape.Circle",
    "radius" to 5.0,
))
// → Shape.Circle(radius=5.0)

Custom identifiers with @SerialName

Fully qualified names are verbose. Use @SerialName for cleaner discriminator values:

import kotlinx.serialization.SerialName

sealed class Event {
    @SerialName("user.created")
    data class UserCreated(val userId: String) : Event()

    @SerialName("user.deleted")
    data class UserDeleted(val userId: String) : Event()
}

codec.slumber(Event.UserCreated("alice"))
// → { "_type": "user.created", "userId": "alice" }

Custom discriminator field

Don't want _type? Implement Polymorphic.Parent on the companion:

sealed class Vehicle {
    companion object : Polymorphic.Parent {
        override val discriminator = "kind"

        // childTypes has no default — supply it, usually via indexedSubClasses()
        override val childTypes: Set<KClass<*>> get() = indexedSubClasses()
    }

    data class Car(val seats: Int) : Vehicle() {
        companion object : Polymorphic.Child {
            override val identifier = "car"
        }
    }

    data class Bike(val gears: Int) : Vehicle() {
        companion object : Polymorphic.Child {
            override val identifier = "bike"
        }
    }
}

codec.slumber(Vehicle.Car(5))
// → { "kind": "car", "seats": 5 }

Default fallback type

When the discriminator is missing or unknown, fall back to a default type:

sealed class Message {
    companion object : Polymorphic.Parent {
        override val defaultType = Text::class

        override val childTypes: Set<KClass<*>> get() = indexedSubClasses()
    }

    data class Text(val body: String) : Message()
    data class Image(val url: String) : Message()
}

// No _type field? Falls back to Text
codec.awake<Message>(mapOf("body" to "Hello!"))
// → Message.Text(body="Hello!")

Migration aliases with @AdditionalSerialName

Renamed a type? Old discriminator values still work:

sealed class Notification {
    @SerialName("email")
    @AdditionalSerialName("email_notification")  // old name
    @AdditionalSerialName("EmailNotification")   // even older name
    data class Email(val to: String) : Notification()
}

// All three discriminators resolve to the same class:
codec.awake<Notification>(mapOf("_type" to "email", "to" to "a@b.com"))
codec.awake<Notification>(mapOf("_type" to "email_notification", "to" to "a@b.com"))
codec.awake<Notification>(mapOf("_type" to "EmailNotification", "to" to "a@b.com"))
// → all return Notification.Email(to="a@b.com")

Polymorphic collections

Lists and maps of polymorphic types work naturally:

sealed class Shape {
    data class Circle(val radius: Double) : Shape()
    data class Rectangle(val width: Double, val height: Double) : Shape()
}

data class Drawing(val shapes: List<Shape>)

val drawing = Drawing(listOf(
    Shape.Circle(5.0),
    Shape.Rectangle(10.0, 20.0),
))

val data = codec.slumber(drawing)
// → { "shapes": [
//     { "_type": "...Circle", "radius": 5.0 },
//     { "_type": "...Rectangle", "width": 10.0, "height": 20.0 }
// ]}

val restored = codec.awake<Drawing>(data)
// → Drawing with the correct subtypes restored

Nested sealed hierarchies

Sealed hierarchies can be nested — Slumber traverses them correctly:

sealed class Animal {
    sealed class Domestic : Animal() {
        data class Dog(val name: String) : Domestic()
        data class Cat(val name: String) : Domestic()
    }
    sealed class Wild : Animal() {
        data class Wolf(val pack: String) : Wild()
    }
}

// All leaf types serialize and deserialize correctly
codec.awake<Animal>(mapOf(
    "_type" to Animal.Domestic.Dog::class.qualifiedName,
    "name" to "Rex",
))
// → Animal.Domestic.Dog(name="Rex")