Skip to Content
Course content

165: Polymorphic Serialization

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this happen to almost every dev transitioning to kotlinx.serialization. You've got a clean class hierarchy, your data models look great, and then you try to deserialize a list of objects and the whole app crashes with a SerializationException. It usually looks something like this:

val paymentMethods: List<PaymentMethod> = listOf(
    CreditCard("1234...", "12/25"),
    PayPal("user@example.com")
)

val jsonString = Json.encodeToString(paymentMethods)
// This might work, but the crash happens here:
val decoded = Json.decodeFromString<List<PaymentMethod>>(jsonString) 
// 💥 SerializationException: Serializer for subclass 'CreditCard' is not found

The Mystery of the Missing Subclass

The problem is that Json.decodeFromString is looking for a serializer for PaymentMethod. If PaymentMethod is an interface or an abstract class, the library has no idea which concrete implementation to instantiate when it sees a JSON object. It doesn't just "guess" based on the fields present; it needs an explicit mapping. Even if you've marked the subclasses as @Serializable, the base type doesn't automatically know about them at runtime during the decoding process.

Using Sealed Classes for Automatic Discovery

The easiest way to fix this—and the way I almost always recommend—is to use a sealed class. Because sealed classes have a closed hierarchy known at compile time, kotlinx.serialization can automatically generate a "polymorphic serializer."

@Serializable
sealed class PaymentMethod {
    @Serializable
    data class CreditCard(val number: String, val expiry: String) : PaymentMethod()
    
    @Serializable
    data class PayPal(val email: String) : PaymentMethod()
}

When you do this, the library adds a special field to the JSON called type (by default). If you encode a CreditCard, the JSON will look like {"type": "com.example.PaymentMethod.CreditCard", "number": "...", "expiry": "..."}. When decoding, the library reads that type field first, looks up the corresponding subclass, and then populates the data. It's seamless, provided you control the class hierarchy.

Customizing the Type Discriminator

In a real-world API, your backend probably isn't sending the full Kotlin class path as the type. They're likely sending something simple like "cc" or "paypal". You can override the default behavior using the @SerialName annotation on your subclasses.

@Serializable
sealed class PaymentMethod {
    @Serializable
    @SerialName("cc")
    data class CreditCard(val number: String, val expiry: String) : PaymentMethod()
    
    @Serializable
    @SerialName("paypal")
    data class PayPal(val email: String) : PaymentMethod()
}

Now your JSON is much cleaner: {"type": "cc", "number": "..."}. I find this is usually where most production projects land. It decouples your internal Kotlin class names from the external API contract, so you can rename your classes without breaking the JSON parser.

Dealing with Open Hierarchies

Now, what if you can't use a sealed class? Maybe the subclasses are defined in different modules, or you're using an interface from a library you don't own. In those cases, you have to manually register the subclasses using a SerializersModule.

val paymentModule = SerializersModule {
    polymorphic(PaymentMethod::class) {
        subclass(CreditCard::class)
        subclass(PayPal::class)
    }
}

val json = Json { 
    serializersModule = paymentModule 
}


By passing this custom Json instance around, you're explicitly telling the engine: "Whenever you encounter a PaymentMethod, these are the possible concrete types you should check for." It's more boilerplate than the sealed class approach, but it's the only way to handle truly open polymorphism.




📋 Practical Task

Implementing a Polymorphic Notification System

You are building a notification dispatcher. You need to handle different types of notifications (Email and SMS) using a single list, and you must ensure the JSON representation uses short, custom identifiers instead of full class names.

Requirements:

  • Create a sealed class Notification.
  • Create two subclasses: EmailNotification (with emailAddress and subject) and SmsNotification (with phoneNumber and message).
  • Use @SerialName to ensure the type discriminator for email is "email" and for SMS is "sms".
  • Write a small main function that:
    1. Creates a List<Notification> containing one of each type.
    2. Serializes the list to a JSON string.
    3. Deserializes that string back into a list and prints the result to verify it works.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.