Skip to content
mac-system-tray

Stop Saying Kotlin Serialization Is Hard to Use!

I've seen many developers—especially those with a Java background—complain that Kotlin Serialization is hard to use, often comparing it with Jackson or Gson. This impression usually stems from a misunderstanding of how it works. Kotlin Serialization doesn't rely on runtime reflection to perform serialization and deserialization.

This design choice is deliberate: Kotlin is a multiplatform language, meaning that the same codebase can compile to the JVM, Android, Native, JavaScript, and more. Since reflection has inconsistent or even nonexistent support across these platforms, Kotlin Serialization takes a smarter route: it uses a compiler plugin to generate serialization code at compile time.

This approach brings several significant advantages:

  1. Cross-platform compatibility: Works across all supported platforms.
  2. Better performance: Avoids the overhead of runtime reflection.
  3. Compile-time type safety: Serialization errors can be caught early during compilation.

Let’s now see how to use Kotlin Serialization in a Compose Multiplatform project.

You can find a Compose Multiplatform setup guide in my previous article. All source code is based on my open-source project crosspaste-desktop.

Quick Start

1. Gradle Configuration

First, add the Kotlin Serialization plugin and dependencies to your project.

gradle/libs.versions.toml

toml
[versions]
kotlin = "2.0.21"

[libraries]
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }

[plugins]
kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }

composeApp/build.gradle.kts

kotlin
plugins {
    ...
    alias(libs.plugins.kotlinSerialization)
    ...
}

kotlin {
    sourceSets {
        commonMain.dependencies {
            implementation(libs.kotlinx.serialization.json)
        }
    }
}

2. Basic Usage

Here’s a simple example with unit tests to demonstrate the basic functionality of Kotlin Serialization.

kotlin
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerialName
import kotlinx.serialization.encodeToString
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlin.test.Test
import kotlin.test.assertEquals

@Serializable
data class User(
    @SerialName("_id")
    val id: Int,
    val name: String,
    val email: String
)

class JsonTest {

    @Test
    fun testJson() {
        val user = User(1, "John Doe", "john@example.com")

        val jsonString = Json.encodeToString(user)
        assertEquals(
            "{\"_id\":1,\"name\":\"John Doe\",\"email\":\"john@example.com\"}", 
            jsonString
        )

        val decodedUser = Json.decodeFromString<User>(jsonString)
        assertEquals(user, decodedUser)
    }
}

3. Custom Serialization

kotlin
import kotlinx.datetime.LocalDateTime
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json
import kotlin.test.Test
import kotlin.test.assertEquals

@Serializable
data class Post(
    val id: Int,
    val title: String,
    @Serializable(with = LocalDateTimeIso8601Serializer::class)
    val createTime: LocalDateTime
)

object LocalDateTimeIso8601Serializer : KSerializer<LocalDateTime> {
    override val descriptor = PrimitiveSerialDescriptor("LocalDateTime", PrimitiveKind.STRING)

    override fun serialize(encoder: Encoder, value: LocalDateTime) {
        encoder.encodeString(value.toString())
    }

    override fun deserialize(decoder: Decoder): LocalDateTime {
        return LocalDateTime.parse(decoder.decodeString())
    }
}

class CustomJsonTest {
    @Test
    fun testCustomSerializer() {
        val post = Post(1, "Hello", LocalDateTime(2021, 1, 1, 12, 0))

        val jsonString = Json.encodeToString(post)
        assertEquals(
            "{\"id\":1,\"title\":\"Hello\",\"createTime\":\"2021-01-01T12:00\"}", 
            jsonString
        )

        val decodedPost = Json.decodeFromString<Post>(jsonString)
        assertEquals(post, decodedPost)
    }
}

4. Polymorphic Serialization

Polymorphic serialization is essential when working with inheritance. This is especially useful for plugin systems, data transport, or persistent storage.

JsonUtils.desktop.kt

kotlin
object DesktopJsonUtils : JsonUtils {

    override val JSON: Json =
        Json {
            encodeDefaults = true
            ignoreUnknownKeys = true
            serializersModule =
                SerializersModule {
                    polymorphic(PasteItem::class) {
                        subclass(ColorPasteItem::class)
                        subclass(FilesPasteItem::class)
                        subclass(HtmlPasteItem::class)
                        subclass(ImagesPasteItem::class)
                        subclass(RtfPasteItem::class)
                        subclass(TextPasteItem::class)
                        subclass(UrlPasteItem::class)
                    }
                }
        }
}

SyncPasteCollection.kt

kotlin
@Serializable
data class SyncPasteCollection(
    val pasteItems: List<PasteItem>,
)

Key Points:

  • PasteItem is a base class for all paste types.
  • Each subtype (e.g., TextPasteItem, ImagePasteItem) represents a specific content type.
  • Serialization configuration ensures the correct type can be encoded and decoded.
  • Works well for networking or database storage of heterogeneous lists.

This enables CrossPaste to elegantly serialize and deserialize mixed-type clipboard items between devices.

Conclusion

Kotlin Serialization was designed with one goal in mind: to offer a unified serialization strategy across all Kotlin platforms. By relying on compile-time code generation rather than runtime reflection, it achieves:

  • Cross-platform support
  • High performance
  • Compile-time type safety

However, the right serialization library depends on your project:

  • For JVM-only projects:

    • Jackson or Gson might be easier to use.
    • They offer a richer ecosystem and simpler out-of-the-box usage.
  • For Kotlin Multiplatform projects:

    • Kotlin Serialization is the optimal choice.
    • Deep integration with Kotlin language features
    • Single codebase support across multiple targets

Ultimately, Kotlin Serialization is not here to replace Jackson or Gson. It solves a different problem: how to do serialization right in a multiplatform world. Once you understand this, it's no longer fair to compare them one-to-one. The right tool always depends on your specific project needs.