Kotlin Map Serialization and Deserialization with Jackson


kotlin map Jackson
Photo by Марьян Блан | @marjanblan on Unsplash

In this article, we will illustrate how to serialize and deserialize a Map in Kotlin which is having complex objects as a key.
Jackson supports only simple objects that are primitive types as a key of the map by default. But usually, it won’t be the case. In your application, you might need complex objects to represent the key of a map. so let's see how to solve this problem.
First, we need to add a Gradle dependency in the build.gradle.kts file of the project. The latest version of jackson-module-kotlin can be found on Maven central.
implementation("com.fasterxml.jackson.module", "jackson-module-kotlin", "2.11.0")
Let's define the simple kotlin data classes Actor and Movie and Rating for our example.
data class Actor(
    val name: String,
    val bornYear: Int,
    val movies: Map<Movie, Rating>
)

data class Movie(
    val title: String,
    val releasingYear: Int,
    val genre: String
)data class Rating(
    val stars: Double,
    val votes: Long
)
As you can see, our Actor data class holds data about movies and their ratings on the map. Here, the key of the map is defined by complex object i.e. Movie.

Serialization

Let’s start with the Serialization of our Actor data class.
In order to serialize/deserialize the data classes, we need an instance of ObjectMapper class for Kotlin and register KotlinModule.
import com.fasterxml.jackson.module.kotlin.KotlinModuleval kMapper = ObjectMapper().registerModule(KotlinModule())
Now we have created ObjectMapper instance, let's use it to serialize our Actor data class.
val robertDowneyJrActor = Actor(
    "Rober Downey Jr",
    1965,
    mapOf(
        Movie("Iron Man", 2008, "Action") to Rating(4.9, 2000L),
        Movie("The Avengers", 2012, "Action") to Rating(4.5, 1800L),
        Movie("Avengers: Endgame", 2019, "Action") to Rating(4.3, 1300L)
    )
)
If we try to serialize robertDowneyJrActor object with below code
val serializedRDJ = kMapper.writeValueAsString(robertDowneyJrActor)
here writeValueAsString method is used to serialize data class to JsonString.
It won’t throw any error, but by default, the mapper will serialize our map key Movie as a String by using toString method of data class (means it won’t be truly JSON representation of Movie key object) and it will cause an error while deserializing it.
To fix this, we need to define a custom serializer for the map key object. Jackson provides a way to define it using @JsonDeserialize annotation and keyUsing to assign custom serializer class.
So first create custom serializer class MoviesMapKeySerializer as
class MoviesMapKeySerializer: JsonSerializer<Movie>() {
    @Throws(IOException::class, JsonProcessingException::class)
    override fun serialize(value: Movie?, gen: JsonGenerator?, serializers: SerializerProvider?) {
        gen?.let { jGen ->
            value?.let { movie ->
                jGen.writeFieldName(kMapper.writeValueAsString(movie))
            } ?: jGen.writeNull()
        }
    }
}
Now update the Actor data class, to annotate the movie map property.
data class Actor(
    val name: String,
    val bornYear: Int,
    @JsonSerialize(keyUsing = MoviesMapKeySerializer::class)
    val movies: Map<Movie, Rating>
)

Deserialization

Now, let’s also define the custom Deserializer for map key objects to deserialize JsonString to data class again.
class MoviesMapKeyDeserializer: KeyDeserializer() {
    @Throws(IOException::class, JsonProcessingException::class)
    override fun deserializeKey(key: String?, ctxt: DeserializationContext?): Movie? {
        return key?.let { kMapper.readValue<Movie>(key) }
    }
}
Also, annotate the movies map property of the Actor class using @JsonDeserialize and keyUsing to assign deserializer class.
data class Actor(
    val name: String,
    val bornYear: Int,
    @JsonDeserialize(keyUsing = MoviesMapKeyDeserializer::class)
    @JsonSerialize(keyUsing = MoviesMapKeySerializer::class)
    val movies: Map<Movie, Rating>
)
Finally, to deserialize, we will use readValue method and pass the type of the object.
val deserializedRDJ = kMapper.readValue<Actor>(jsonRDJ)
In this article, we have learned how to deal map with complex objects as key while serializing and deserializing using Jackson.

Comments