Kotlin

Sealed Classes Serialization And Deserialization with Kotlinx Serialization Library

Google+ Pinterest LinkedIn Tumblr

Recently, I have been working on a project where I need to store sealed class data inside the Room Persistence using the Kotlinx Serialization library. We all know that Room only supports primitives types for storage. I struggle for almost two days just store the sealed classes using the Kotlinx Serialization library.

So, in this article, I’ll walk through you how to serialize and deserialize the sealed classes using the Kotlinx Serialization library.

To find out the basics of Kotlinx Serialization check out the following article.

Gradle Stuff

Kotlinx Serialization and Room Persistence require us to add some more stuff to our application module build.gradle file. We start by adding the following dependencies.

apply plugin: 'kotlin-kapt' // kapt plugin for code generation
apply plugin: 'kotlinx-serialization'   // kotlin serialization plugin

 dependencies {   
    // Room architecture component dependencies
    implementation 'androidx.room:room-runtime:2.1.0'
    kapt 'androidx.room:room-compiler:2.1.0'
    implementation 'androidx.room:room-ktx:2.1.0'

    // Kotlinx Serialization
    implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.13.0"
}

We also need to add a classpath for Kotlinx Serialization library inside the top-level build.gradle file inside the buildScript->dependencies section.

buildscript {
    ext.kotlin_version = '1.3.40'
    repositories {
        google()
        jcenter()
    }
    dependencies {
        ......
        classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
    }
}

Structure of sealed class

The structure of my sealed class is pretty simple. I have one top-level sealed class and inside that class, there are several inheritance hierarchies. First, let’s see the structure.

sealed class AnsweredContent {

    @Serializable
    data class SingleTextContent(var answer: String = "") : AnsweredContent()

    @Serializable
    class MultiChoiceContent (val choices : List<String> = listOf<String>()) : AnsweredContent() {

    @Serializable
    data class LocationContent(var latitude: Double = 0.0, var longitude: Double = 0.0, var accuracy: Float = 0F) : AnsweredContent() 
}

Update: With the release of 0.13.0 version of kotlinx-serialization we just need to annotate our inner classes with @Serializable annotation.

Now let’s say I have this following class which holds the instance of my AnsweredContent sealed class.

@Serializable
data class AnsweredContentHolder (
   val id: Int,
   val title: String,
   @Polymorphic val answeredContent: AnsweredContent
)

The @Polymorphic serialization annotation mostly used on sealed, abstract classes, and on interfaces as well.

Here is the definition of @Polymorphic annotation.

This annotation is applied to interfaces and serializable abstract classes and can be applied to open classes as well. It does not affect sealed classes because they are be serialized with sub-classes automatically.

Now to actually use this in action and convert the AnsweredContentHolder into JSON and back to object. We need to write a custom serializer for this.

Writing custom serializer

For the sake of simplicity, I have created a BaseSerializer<T> and define the generics toStringT, toTString method there. Have each custom serializer extends the BaseSerializer and add methods specific to each of them.

interface BaseSerializer<T> {

    val serializer: KSerializer<T>

    val format: StringFormat

    fun toStringT(json: String): T

    fun toTString(t: T): String
}

And here is the implementation class of our generic serializer.

class AnsweredContentSerializer : BaseSerializer<AnsweredContentHolder> {

    private val sealedModule = SerializersModule {  // 1
        polymorphic(AnsweredContent::class) {
            AnsweredContent.SingleTextContent::class with AnsweredContent.SingleTextContent.serializer()
            AnsweredContent.MultiChoiceContent::class with AnsweredContent.MultiChoiceContent.serializer()
            AnsweredContent.LocationContent::class with AnsweredContent.LocationContent.serializer()
        }
    }

    override val serializer: KSerializer<AnsweredContentHolder>
        get() = AnsweredContentHolder.serializer()  // 2

    override val format: StringFormat
        get() = Json(context = sealedModule)  // 3

    override fun toStringT(json: String): AnsweredContentHolder {
        return format.parse(serializer, json)  // 4
    }

    override fun toTString(t: AnsweredContentHolder): String {
        return format.stringify(serializer, t)  // 5
    }
}

Here is the step-by-step implementation of the above code.

  1. If you remembered our AnsweredContent class has the @Polymorphic annotation on it. So, for the correct serialization and deserialization, we need to tell KSerializer how to do serialize and deserialize the AnsweredContent using the SerializersModule.
  2. Supply the serializer for AnsweredContentHolder because this class holds the reference of our AnsweredContent instance.
  3. Returning the instance of JSON by passing the sealedModule.
  4. Parse the json string into AnsweredContentHolder class instance.
  5. Convert the object into the string using the StringFormat.

Testing kotlinx serialization and deserialization with Object

In order to test the serialization and deserialization, we just need to get the instance of AnsweredContentSerializer and pass the valid parameter to its function. Let’s see the example.

fun main() {
    val answeredContentHolder = AnsweredContentHolder(1, "Ahsen Saeed", AnsweredContent.LocationContent(34.515451, 74.568651, 22F))
    val serializer = AnsweredContentSerializer()
    val json = serializer.toTString(answeredContentHolder)
    val newAnsweredContent = serializer.toStringT(json)
    println("Json content -> $json")
    println("Answered Content -> $newAnsweredContent")
}

// The output of above program
Json content -> {"id":1,"title":"Ahsen Saeed","answeredContent":{"type":"AnsweredContent.LocationContent","latitude":34.515451,"longitude":74.568651,"accuracy":22.0}}
Answered Content -> AnsweredContentHolder(id=1, title=Ahsen Saeed, answeredContent=LocationContent(latitude=34.515451, longitude=74.568651, accuracy=22.0))

Testing kotlinx serialization and deserialization with Collection

Now let’s say if I need to serialize and deserialize the list of AnsweredContentHolder using our generic serializer. So, for that, we need to update our AnsweredContentSerializer and pass the List<AnsweredContentHolder> instead of simple class for generic.

class AnsweredContentSerializer : BaseSerializer<List<AnsweredContentHolder>> {

    private val sealedModule = SerializersModule {
        polymorphic(AnsweredContent::class) {
            AnsweredContent.SingleTextContent::class with AnsweredContent.SingleTextContent.serializer()
            AnsweredContent.MultiChoiceContent::class with AnsweredContent.MultiChoiceContent.serializer()
            AnsweredContent.LocationContent::class with AnsweredContent.LocationContent.serializer()
        }
    }

    override val serializer: KSerializer<List<AnsweredContentHolder>>
        get() = AnsweredContentHolder.serializer().list

    override val format: StringFormat
        get() = Json(context = sealedModule)

    override fun toStringT(json: String): List<AnsweredContentHolder> {
        return format.parse(serializer, json)
    }

    override fun toTString(t: List<AnsweredContentHolder>): String {
        return format.stringify(serializer, t)
    }
}

The only noticeable thing in the above code is, you see we’re returning KSerializer<List<AnsweredContentHolder>> instead of simple KSerializer.

And here’s how to convert the collection into string and back to collection.

fun main() {
    val answeredContentHolders = listOf(AnsweredContentHolder(1, "Ahsen Saeed", AnsweredContent.LocationContent(34.515451, 74.568651, 22F)), AnsweredContentHolder(2, "Coding Infinite", AnsweredContent.SingleTextContent("Hello World")))
    val serializer = AnsweredContentSerializer()
    val json = serializer.toTString(answeredContentHolders)
    val newAnsweredContents = serializer.toStringT(json)
    println("Json content -> $json")
    println("Answered Content -> $newAnsweredContents")
}

// The output of above program
Json content -> [{"id":1,"title":"Ahsen Saeed","answeredContent":{"type":"AnsweredContent.LocationContent","latitude":34.515451,"longitude":74.568651,"accuracy":22.0}},{"id":2,"title":"Coding Infinite","answeredContent":{"type":"AnsweredContent.SingleTextContent","answer":"Hello World"}}]
Answered Content -> [AnsweredContentHolder(id=1, title=Ahsen Saeed, answeredContent=LocationContent(latitude=34.515451, longitude=74.568651, accuracy=22.0)), AnsweredContentHolder(id=2, title=Coding Infinite, answeredContent=SingleTextContent(answer=Hello World))]

Alright, guys, this was my understanding of how to serialize and deserialize the sealed classes through Kotlinx Serialization library. If you any queries or suggestion on improving the above example please let me know in the comments section.

Thank you for being here and keep reading…

2 Comments

    • ahsensaeed067 Reply

      Hey faith,
      When I wrote this post using the 0.11.0 version this is the best solution I came up with in order to serialize & deserialize the sealed classes. I’ll update my article according to the newly released version.

      Also, good to hear that in the future the library will be capable of serializing sealed classes internally.

Write A Comment