Kotlin with Jackson: Deserializing Kotlin Sealed Classes
Recently I needed to deserialize Kotlin sealed class into a CSV. Let’s look at how this can be done.
According to official Kotlin reference guide for Sealed Classes:
Sealed classes are used for representing restricted class hierarchies, when a value can have one of the types from a limited set, but cannot have any other type. They are, in a sense, an extension of enum classes: the set of values for an enum type is also restricted, but each enum constant exists only as a single instance, whereas a subclass of a sealed class can have multiple instances which can contain state.
They are quite handy indeed and when I’m writing in Kotlin I generally prefer sealed classes over enums. I will not discuss sealed classes themselves in this article, let’s look at the limitation I faced when deserializing a sealed class into CSV file.
Consider the following class:
data class Data(
val id: String,
val parameterType: String,
val parameterValue: String
)
And a CSV schema to deserialize from:
id,parameterType,parameterValue
If we want to restrict our parameter types (which is usually a good idea) we could use an enum class or a sealed class:
data class Data(
val id: String,
val parameterType: Parameter,
val parameterValue: String
)sealed class Parameter(val value: String)object FirstName: Parameter("First Name")
object FullName : Parameter("Full Name")
We have our Parameter
class, which is a sealed class
and two objects implementing it FirstName
and FullName
.
This might not be the best case to use sealed classes. After all, both our implementations are object
‘s and therefore can also exist only as a single instance (same as enums), however for simplicity’s sake let’s keep it like this.
Sealed classes come with some restrictions though, among which:
A sealed class is abstract by itself, it cannot be instantiated directly
Therefore Jackson will not be able to instantiate our Parameter
class directly, and if we try to deserialize our csv data to this class we will get:
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `Parameter` (no Creators, like default construct, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
I didn’t want to create a custom deserializer for this case, this seems like an overkill to me. Therefore I looked into implementing a creator function for the sealed class:
sealed class Parameter(val value: String) {companion object {
@JsonCreator
@JvmStatic
private fun creator(name: String): Parameter? {
// some logic here
}
}
}
@JsonCreator
is an annotation from Jackson:
Marker annotation that can be used to define constructors and factory methods as one to use for instantiating new instances of the associated class.
and @JvmStatic
is an annotation from Kotlin that allows us to have this function generated as real static method for Java (See java interop for more details.)
So what do we put inside the creator()
function?
If it were an enum class we could, for example, deserialize using the enum class name:
enum class EnumParameter(val value: String) {
FIRST_NAME("Fist Name"),
FULL_NAME("Full Name");companion object {
@JsonCreator
@JvmStatic
private fun creator(name: String): EnumParameter? {
return EnumParameter.values().firstOrNull { it.name == name }
}
}
}
And our CSV to deserialize from would look something like this:
id,parameterName,parameterValue
1,FIRST_NAME,Jane
2,FULL_NAME,John Smith
Can we do the same with sealed classes? Yes! Luckily since Kotlin 1.3 we have sealedSubclasses
property for KClass
type that returns the list of the immediate subclasses if this class is a sealed class.
So using some reflection magic the above code for enum class converted to work with a sealed class would look like this:
sealed class Parameter(val value: String) {companion object {
@JsonCreator
@JvmStatic
private fun creator(name: String): Parameter? {
return Parameter::class.sealedSubclasses.firstOrNull { it.simpleName == name }?.objectInstance
}
}
}
And our CSV:
id,parameterName,parameterValue
1,First Name,Jane
2,Full Name,John Smith
Unfortunately if we’re using Kotlin 1.2 or lower we don’t have the sealedSubclasses
property available and I haven’t found any other way how to let Jackson know which implementation of the sealed class to instantiate. If you know a way please let me know and I will update the article.