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")