Kotlinx Serialization vs Jackson

My defacto library for serialization and deserialization is Jackson. However, I’ve been using Gson for android applications. Maybe the misleading thought that android and Gson go well together. Because it’s backed by the same big company. Then I heard that there is kotlinx serialization which is getting momentum recently. So this is a practical comparison of kotlinx serialization vs Jackson. Please note that I am not going to compare the performance or any other aspect. Finally, I’ll tell you about my thoughts on using kotlinx serialization.

Kotlinx Serialization

According to the official project, kotlinx serialization consists of a compile time code generator and runtime library. Currently, it supports JSON, CBOR, and ProtoBuf officially. Also, it has support for several formats with community projects.

Jackson

Jackson is a very popular library in java(JVM) ecosystem. It has support for JSON, BSON, CBOR and many others. I am using Jackson for a very long time and found it has a less steep learning curve. But I’ve noticed it has some conflicting issues over versions. I faced some issues wherein a large scale project many libraries use particular Jackson versions. As a result, they may get end up in the conflicting state. Other than that it’s a cool library to use.

What to expect?

I am going to compare these two starting from simple type to polymorphic type serialization. I am using Maven as the build tool and kotlin as the language. Jackson works well with kotlin, so no issue there. The format I am going to use here is JSON. So let’s get started.

Project Setup

Following kotlinx serialization and Jackson dependencies are required. And I am using Junit 5, you can use whatever unit testing framework.

<dependency>
    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-stdlib</artifactId>
    <version>1.3.72</version>
</dependency>
<dependency>
    <groupId>org.jetbrains.kotlinx</groupId>
    <artifactId>kotlinx-serialization-runtime</artifactId>
    <version>0.20.0</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>2.11.0</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-annotations</artifactId>
    <version>2.11.0</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.11.0</version>
</dependency>

Next, we need to configure the Kotlin compiler plugin to use kotlinx serialization. So it can generate the required code during compilation.

<plugin>
    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-maven-plugin</artifactId>
    <version>1.3.72</version>
    <configuration>
        <compilerPlugins>
            <plugin>kotlinx-serialization</plugin>
        </compilerPlugins>
    </configuration>
    <dependencies>
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-maven-serialization</artifactId>
            <version>1.3.72</version>
        </dependency>
    </dependencies>
    <executions>
        <execution>
            <id>compile</id>
            <phase>compile</phase>
            <goals>
                <goal>compile</goal>
            </goals>
        </execution>
        <execution>
            <id>test-compile</id>
            <phase>test-compile</phase>
            <goals>
                <goal>test-compile</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Add these to your pom. After that, the project is ready. If you want to follow along, add Junit 5 dependencies also.

Kotlinx Serialization Vs Jackson For Simple Data Class And Collection

Kotlinx Serialization

Let’s have a look at how kotlinx serialization works for a simple Kotlin data class.

package com.aptkode.model

import kotlinx.serialization.Serializable

@Serializable
data class User(val name: String, val age: Int)

Note that we have used @Serializable annotation from kotlinx. serialization package which enables code generation. The plugin injects a special function serializer() to the companion object of the class. So we can use it without much trouble. Let’s see those in action in the following tests.

@Test
fun serializeTest(){
    val json = Json(JsonConfiguration.Stable)
    val jsonString = json.stringify(User.serializer(), User("tom", 21))
    assertEquals("""{"name":"tom","age":21}""", jsonString)
}

@Test
fun deserializeTest(){
    val json = Json(JsonConfiguration.Stable)
    val (name, age) = json.parse(User.serializer(), """{"name":"john","age":22}""")
    assertEquals("john",name)
    assertEquals(22,age)
}

.serializer() method returns a KSerializer of particular type. It has a list, set for collection serialization and deserialization. Also, it has nullable to deal with null scenarios.

@Test
fun serializeListTest(){
    val json = Json(JsonConfiguration.Stable)
    val jsonString = json.stringify(User.serializer().list, listOf(
            User("tom", 21),
            User("john", 22)
    ))
    assertEquals("""[{"name":"tom","age":21},{"name":"john","age":22}]""", jsonString)
}

Jackson

Let’s look at the same scenarios using Jackson.

@Test
fun serializeTest(){
    val objectMapper = ObjectMapper()
    val jsonString = objectMapper.writeValueAsString(User("tom", 21))
    assertEquals("""{"name":"tom","age":21}""", jsonString)
}

No much difference, right? Yes but deserialization will give us com.fasterxml.jackson.databind.exc.InvalidDefinitionException exception if we try the objectMapper.readValue method without a change to object mapper.

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.aptkode.model.User` (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
 at [Source: (String)"{"name":"john","age":22}"; line: 1, column: 2]

That is because we are using the kotlin data class. So we need to add Jackson kotlin support. Add the following dependency to the pom.

<dependency>
    <groupId>com.fasterxml.jackson.module</groupId>
    <artifactId>jackson-module-kotlin</artifactId>
    <version>2.10.4</version>
</dependency>

Now we can use KotlinModule as follows and Jackson will happily deserialize the JSON string for us.

@Test
fun deserializeTest(){
    val objectMapper = ObjectMapper()
    objectMapper.registerModule(KotlinModule())
    val (name, age) = objectMapper.readValue("""{"name":"john","age":22}""", User::class.java)
    assertEquals("john",name)
    assertEquals(22,age)
}

Now let’s look at the list serialization. It can be done without hassle.

@Test
fun serializeListTest(){
    val objectMapper = ObjectMapper();
    val jsonString = objectMapper.writeValueAsString(listOf(
            User("tom", 21),
            User("john", 22)
    ))
    assertEquals("""[{"name":"tom","age":21},{"name":"john","age":22}]""", jsonString)
}

For deserialization, there are several options but I’d like to use the following approach.

@Test
fun deserializeListTest(){
    val objectMapper = ObjectMapper()
    objectMapper.registerModule(KotlinModule())
    val users = objectMapper.readValue("""[{"name":"tom","age":21},{"name":"john","age":22}]""", Array<User>::class.java)
    assertEquals(2, users.size)
}

We’ve done serialization and deserialization for a simple class. For both methodologies, there is no much of a difference.

Kotlinx Serialization Vs Jackson Polymorphic Type Serialization and Deserialization

I am using the following inheritance hierarchy to form polymorphic relationships. Please note that classes are annotated with both Kotlin and Jackson annotations.

package com.aptkode.model

import com.fasterxml.jackson.annotation.JsonTypeInfo
import kotlinx.serialization.Serializable

@Serializable
@JsonTypeInfo(use=JsonTypeInfo.Id.CLASS, include= JsonTypeInfo.As.PROPERTY, property="type")
abstract class Animal {
    abstract fun sound(): String
    abstract fun legs(): Int
}
package com.aptkode.model

import kotlinx.serialization.Serializable

@Serializable
data class Cat(val sound: String = "meow", val legs: Int = 4) : Animal() {

    override fun sound(): String {
        return sound
    }

    override fun legs(): Int {
        return legs
    }
}
package com.aptkode.model

import kotlinx.serialization.Serializable

@Serializable
data class Dog(val sound: String = "Baw", val legs: Int = 4) : Animal() {
    override fun sound(): String {
        return sound
    }

    override fun legs(): Int {
        return legs
    }
}

Kotlinx Serialization and Deserialization

Kotlinx provides a DSL type of constructs to handle polymorphic serialization. We need to build SerializersModuleBuilder using the DSL. Then we can pass it as the context to the JSON object.

@Test
fun polymorphicSerializeTest() {
    val animalModule = SerializersModule {
        polymorphic(Animal::class) {
            Cat::class with Cat.serializer()
            Dog::class with Dog.serializer()
        }
    }
    val animals = listOf(Cat(), Cat(), Dog())
    val json = Json(JsonConfiguration.Stable, context = animalModule)
    val jsonString = json.stringify(Animal.serializer().list, animals)
    assertEquals("""[{"type":"com.aptkode.model.Cat","sound":"meow","legs":4},{"type":"com.aptkode.model.Cat","sound":"meow","legs":4},{"type":"com.aptkode.model.Dog","sound":"Baw","legs":4}]""", jsonString)
}

For the deserialization, same module builder can be used. I think you have noticed that new property called type is added. That’s how kotlinx serialization keep metadata about polymorphic types. So we need to give the same type of JSON string to make deserialization work. But you could change the default behaviour by annotating the type with @SerialName. This is a good idea since it doesn’t expose internal class names. 

@Test
fun polymorphicDeserializeTest() {
    val animalModule = SerializersModule {
        polymorphic(Animal::class) {
            Cat::class with Cat.serializer()
            Dog::class with Dog.serializer()
        }
    }
    val json = Json(JsonConfiguration.Stable, context = animalModule)
    val animals = json.parse(Animal.serializer().list, """[{"type":"com.aptkode.model.Cat","sound":"meow","legs":4},{"type":"com.aptkode.model.Cat","sound":"meow","legs":4},{"type":"com.aptkode.model.Dog","sound":"Baw","legs":4}]""")
    println(animals)
}

Jackson serialization and deserialization

For Jackson, I’ve added @JsonTypeInfo annotation to the Animal class. But there are few other ways to do it. One is using global default typing. But for the comparison, I choose this method. By default, Jackson won’t include metadata about classes in the serialized form. So we need to use writerFor method and pass relevant type data.

@Test
fun polymorphicSerializeTest() {
    val objectMapper = ObjectMapper()
    objectMapper.registerModule(KotlinModule())
    val animals: List<Animal> = listOf(Cat(), Cat(), Dog())
    val jsonString = objectMapper
            .writerFor(objectMapper.typeFactory.constructCollectionType(List::class.java, Animal::class.java))
            .writeValueAsString(animals)
    assertEquals("""[{"type":"com.aptkode.model.Cat","sound":"meow","legs":4},{"type":"com.aptkode.model.Cat","sound":"meow","legs":4},{"type":"com.aptkode.model.Dog","sound":"Baw","legs":4}]""", jsonString)
}

For the deserialization, we don’t have to do much. If the serialized form is untouched Jackson will deserialize it without any issue. Same as kotlin here we are also able to change the metadata by using different parameters on the @JsonTypeInfo annotation.

@Test
fun polymorphicDeserializeTest() {
    val objectMapper = ObjectMapper()
    objectMapper.registerModule(KotlinModule())
    val animals: Array<Animal> = objectMapper.readValue("""[{"type":"com.aptkode.model.Cat","sound":"meow","legs":4},{"type":"com.aptkode.model.Cat","sound":"meow","legs":4},{"type":"com.aptkode.model.Dog","sound":"Baw","legs":4}]""", Array<Animal>::class.java)
    assertEquals(3, animals.size)
    assertEquals("com.aptkode.model.Cat", animals[0].javaClass.typeName)
    assertEquals("com.aptkode.model.Dog", animals[2].javaClass.typeName)
}

Final Thoughts

We have looked at simple class serialization and deserialization. There is no much difference in both libraries for a simple application. But Jackson would come in handy if you have to serialize and deserialize classes from third-party libraries. Because Jackson has the notion of Mixins and many other configurable features. Since kotlinx serialization is relatively young these are not available yet.

For the list types, both work great yet Jackson is more mature in configurations and tweaking output.

For polymorphic serialization, kotlinx is doing a good job and Jackson still shines since its maturity. So these are my idea, let me know yours in the comment section. I know you would ask for the project, so here it is. Thanks for reading.

Leave a Comment

Your email address will not be published. Required fields are marked *