---
title: "Intro to Polymorphism with Kotlinx.Serialization"
source: "/writing/intro-to-polymorphism-with-kotlinx-serialization/"
---

Article

# Intro to Polymorphism with Kotlinx.Serialization

David Perez

September 12, 2023

Polymorphic structures can come in many different forms; not all of them are made equally and, in some cases, you may have to deal with multiple types in the same project. Luckily [Kotlinx.Serialization](https://github.com/Kotlin/kotlinx.serialization) has a few tools to handle the most common forms of polymorphic data.

## Modeling Polymorphism

There are 2 different ways you can model polymorphic JSON in code. Both of them have strengths and weaknesses that can be used for different purposes.

-   **Sealed Classes:** Represents sealed (or closed) polymorphism and requires a little less setup to function.
-   **Abstract & Open classes:** Represents open polymorphism and requires extra setup but provides the inherent flexibility of `abstract` classes.

We will start with `sealed` classes and move to `abstract` at the end.

## Class Discriminator

The most straightforward tool when deserializing polymorphic data to sealed classes is the `classDiscriminator`. It requires the least amount of code to make it work but demands that the JSON of your app contain a specific type identifier. The `classDiscriminator` is a property on the `[Json](https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json/)` class that indicates the name of the JSON property that identifies which class variant the JSON should be deserialized to. By default this value is `"type"` but you may want to customize it for your needs.

For example, here is a simple example of an array of polymorphic objects that could be sent to your app:

[
  {
    "type": "subclass_a",
    "id": 12345,
    "dataA": "Hello"
  },
  {
    "type": "subclass_b",
    "id": 54321
    "dataB": 100
  },
  {
    "type": "subclass_a",
    "id": 98765,
    "dataA": "Goodbye"
  }
]

As you can see, there are really two different types of objects here with their own unique data associated with them (`"dataA"` and `"dataB"`, modeled here as a string and integer respectively). Modeling them using a single class including all the possible properties of both would obfuscate the tight relationship between type A objects always containing `"dataA"` and type B objects always containing `"dataB"`. The JSON above can instead be deserialized into a `List<Base>` and maintain their strong relationships as long as the models and the `Json` have the appropriate setup.

Json {
    classDiscriminator = "type"
}

@Serializable
sealed class Base {
    abstract val id: Int

    @Serializable
    @SerialName("subclass_a")
    data class SubclassA(
        @SerialName("id") override val id: Int,
        @SerialName("dataA") val dataA: String,
    ) : Base()

    @Serializable
    @SerialName("subclass_b")
    data class SubclassB(
        @SerialName("id") override val id: Int,
        @SerialName("dataB") val dataB: Int,
    ) : Base()
}

The `@SerialName` annotation on each of the implementing subclasses is matched to the value of the `type` property to tie it all together. This means that when the `type` has a value of `"subclass_a"`, it will attempt to deserialize the model as a `SubclassA` class while a value of `"subclass_b"` will be deserialized to a `SubclassB` class.

## Json Class Discriminator

Hopefully the `classDiscriminator` solution works for your needs but it has limitations. One major one is that the `Json` class can only support a single `classDiscriminator` and this can cause issues if you have inconsistent JSON modeling. Perhaps most of your polymorphic models use the default property key `type` but every once in a while, there is a model that uses `data_type` as the key. There is a pretty simple solution for that too:

Json {
    classDiscriminator = "type"
}

@JsonClassDiscriminator("data_type")
@Serializable
sealed class Base {
    abstract val id: Int

    @Serializable
    @SerialName("subclass_a")
    data class SubclassA(
        @SerialName("id") override val id: Int,
        @SerialName("dataA") val dataA String,
    ) : Base()

    @Serializable
    @SerialName("subclass_b")
    data class SubclassB(
        @SerialName("id") override val id: Int,
        @SerialName("dataB") val dataB: Int,
    ) : Base()
}

Looks identical, except for the one key line. The added annotation, `JsonClassDiscriminator`, allows you to override the default `classDisciminator` and parse models regardless of the value being used. This can be done for any number of your polymorphic models and adds that extra bit of specificity that can be useful for future developers.

## Content Serializer

The discriminator, as defined by the `classDiscriminator` or the `JsonClassDiscriminator`, is a powerful tool but it makes a basic assumption: that there is some sort of `type` property to help identify the specific class you want to be using. This is not always the case and the solutions above will not be able to help you in those scenarios... but there is still a way!

First, let's see some new JSON without a `"type"` property:

[
  {
    "id": 12345,
    "dataA": "Hello"
  },
  {
    "id": 54321
    "dataB": 100
  },
  {
    "id": 98765,
    "dataA": "Goodbye"
  }
]

Our new JSON is simple but you can see that the models still contain unique payloads, the `"dataA"` and `"dataB"` properties that are specific to their subclass. This information is all we need to determine which type of model we want to serialize to.

object BaseSerializer : JsonContentPolymorphicSerializer<Base>(
    Base::class,
) {
    override fun selectDeserializer(
        element: JsonElement,
    ): DeserializationStrategy<Base> {
        val jsonObject = element.jsonObject
        return when {
            jsonObject.containsKey("dataA") -> SubclassA.serializer()
            jsonObject.containsKey("dataB") -> SubclassB.serializer()
            else -> throw IllegalArgumentException(
                "$type is not a supported Base type.",
            )
        }
    }
}

@Serializable(BaseSerializer::class)
sealed class Base {
    ...
}

This code works out well and since you have access to the entire `JsonElement`, you can customize it to use any manner of conditions when determining which serializer to use. There is also nothing stopping you from using this form of serializer when you do have a `type` property if you prefer to have a more hands-on approach when picking your serializer.

Json {
    classDiscriminator = "type"
}

object BaseSerializer : JsonContentPolymorphicSerializer<Base>(
    Base::class,
) {
    override fun selectDeserializer(
        element: JsonElement,
    ): DeserializationStrategy<Base> {
        val json = element.jsonObject
        val type = json.getValue("data_type").jsonPrimitive.content
        return when (type) {
            "subclass_a" -> SubclassA.serializer()
            "subclass_b" -> SubclassB.serializer()
            else -> throw IllegalArgumentException(
                "$type is not a supported Base type.",
            )
        }
    }
}

@Serializable(BaseSerializer::class)
sealed class Base {
    ...
}

Because you are manually taking over the process of parsing type information, one thing to be careful of here is that you must make sure that the `classDiscriminator` does not match otherwise you will get a runtime error when trying to deserialize. In this case, the `classDiscriminator` is set to `type` and our key is `data_type` so there is no conflict.

## Sealed Versus Open

The previous approaches all work well and are exceptionally clear but they only illustrate sealed polymorphism. Open polymorphism is the other option and it is applied when using an `abstract` class or an `open` class. As a simple example, we can take our previous test class and change it to abstract:

Json {
    classDiscriminator = "type"
}

@Serializable
abstract class Base {
    abstract val id: Int

    @Serializable
    @SerialName("subclass_a")
    data class SubclassA(
        @SerialName("id") override val id: Int,
        @SerialName("dataA") val dataA: String,
    ) : Base()

    @Serializable
    @SerialName("subclass_b")
    data class SubclassB(
        @SerialName("id") override val id: Int,
        @SerialName("dataB") val dataB: Int,
    ) : Base()
}

You may expect this to work immediately but unfortunately this will not deserialize the models properly. When using open polymorphism you must register all the subclasses that can be deserialized. It is a relatively simple process but does incur some additional overhead. I suggest using sealed polymorphism when possible as it is the most straightforward to implement and maintain.

If we did want to make this work, we just need to add the registration code to the `Json` class.

Json {
    classDiscriminator = "type"
    serializersModule = SerializersModule {
        polymorphic(Base::class) {
            subclass(Base.SubclassA::class)
            subclass(Base.SubclassB::class)
        }
    }
}

And voila! With the classes registered, everything will work as before. Important to note that any time you add a new implementation of your `abstract` class, you must update your registered classes.

## The Wrap Up

There are multiple ways to handle open and sealed polymorphism when using Kotlinx.Serialization in your app. The handful of tools described above will provide the most flexibility while keeping the complexity as low as possible. No matter what your polymorphic JSON looks like, there should be a way to deserialize it.

_David deserializes all sorts of polymorphic models at_ [_Livefront_](https://livefront.com/)

-   David Perez

    Software Engineer

    [More from David](/team/david-perez/)

    David Perez

### Tags

[Digital Engineering](/insights/all/?topic=digital-engineering) [Mobile Apps](/insights/all/?topic=mobile-apps)

### Share

-   Copied

## View these next.

[See all](/insights/all/?type=writing)

-   [

    Article

    How To Sabotage Your Project Using Inconsistency

    Collin Flynn

    ](/writing/how-to-sabotage-your-project-using-inconsistency/)
-   [

    Article

    Unit Testing race conditions by creating chaos (Swift)

    Sean Berry

    ](/writing/unit-testing-race-conditions-by-creating-chaos-swift/)
-   [

    Article

    UIApplicationDelegate call sequence reference

    Sean Berry

    ](/writing/uiapplicationdelegate-call-sequence-reference/)
