How to serialize/deserialize Kotlin sealed class?

Zbigniew Malinowski picture Zbigniew Malinowski · May 3, 2018 · Viewed 17.2k times · Source

I have a following sealed class:

sealed class ViewModel {

  data class Loaded(val value : String) : ViewModel()
  object Loading : ViewModel()

}

How can I serialize/deserialize instances of the ViewModel class, let's say to/from JSON format?

I've tried to use Genson serializer/deserializer library - it can handle Kotlin data classes, it's also possible to support polymorphic types (eg. using some metadata to specify concrete types).

However, the library fails on Kotlin object types, as these are singletons without a public constructor. I guess I could write a custom Genson converter to handle it, but maybe there's an easier way to do it?

Answer

gil.fernandes picture gil.fernandes · May 3, 2018

You are probably right about the creating a custom serializer.

I have tried to serialize and de-serialize your class using the Jackson library and Kotlin.

These are the Maven dependencies for Jackson:

<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>2.8.8</version>
</dependency>

<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.8.8</version>
</dependency>

You can serialize the sealed class to JSON using this library with no extra custom serializers, but de-serialization requires a custom de-serializer.

Below is the toy code I have used to serialize and de-serialize your sealed class:

import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.module.SimpleModule

sealed class ViewModel {
    data class Loaded(val value: String) : ViewModel()
    object Loading : ViewModel()
}

// Custom serializer
class ViewModelDeserializer : JsonDeserializer<ViewModel>() {
    override fun deserialize(jp: JsonParser?, p1: DeserializationContext?): ViewModel {
        val node: JsonNode? = jp?.getCodec()?.readTree(jp)
        val value = node?.get("value")
        return if (value != null) ViewModel.Loaded(value.asText()) else ViewModel.Loading
    }
}

fun main(args: Array<String>) {
    val m = createCustomMapper()
    val ser1 = m.writeValueAsString(ViewModel.Loading)
    println(ser1)
    val ser2 = m.writeValueAsString(ViewModel.Loaded("test"))
    println(ser2)
    val deserialized1 = m.readValue(ser1, ViewModel::class.java)
    val deserialized2 = m.readValue(ser2, ViewModel::class.java)
    println(deserialized1)
    println(deserialized2)
}

// Using mapper with custom serializer
private fun createCustomMapper(): ObjectMapper {
    val m = ObjectMapper()
    val sm = SimpleModule()
    sm.addDeserializer(ViewModel::class.java, ViewModelDeserializer())
    m.registerModule(sm)
    return m
}

If you run this code this is the output:

{}
{"value":"test"}
ViewModel$Loading@1753acfe
Loaded(value=test)