kotlinx.serialization: What It Is, Annotations, and JSON Serialization

Author: IT Sectr Published: 2026-03-15 Reading time: 12 min

kotlinx.serialization — a multiplatform library from JetBrains for converting Kotlin objects to JSON, ProtoBuf, CBOR, and other formats without using reflection. Unlike Gson and Moshi, it generates serializer code at compile time through the @Serializable annotation, delivering high performance and type safety. According to GitHub Kotlin/kotlinx.serialization, the library supports Kotlin/JVM, Kotlin/Native, Kotlin/JS, and Kotlin/Wasm.

Key Takeaways

  • kotlinx.serialization — compile-time serialization: code is generated at compile time, no reflection used
  • @Serializable — the main annotation that triggers serializer generation for a class
  • Json {} builder — JSON configuration via Json { ignoreUnknownKeys = true; prettyPrint = true }
  • Multiplatform — the library works on JVM, Native, JS, and Wasm without API changes
  • Custom serializers — via the KSerializer interface for non-standard data formats

What Is kotlinx.serialization

kotlinx.serialization is a built-in serialization library for Kotlin, developed by JetBrains as part of the official Kotlin ecosystem. Its main difference from third-party solutions (Gson, Moshi, Jackson) is that it does not use reflection at runtime. Instead, serializer code is generated at compile time using Kotlin Symbol Processing (KSP) or the Kotlin Compiler Plugin. This provides a performance boost of up to 3–5 times compared to Gson and guarantees type safety.

The library officially supports four formats: JSON (via the kotlinx-serialization-json module), ProtoBuf (kotlinx-serialization-protobuf), CBOR (kotlinx-serialization-cbor), and HOCON (kotlinx-serialization-hocon). Formats are added as separate dependencies in build.gradle.kts, so you don’t pull unnecessary libraries into your project. Each format has its own set of configuration parameters.

Multiplatform is a key feature of the library. The same class with @Serializable works on all targets: JVM (Android, Backend), Native (iOS), JS (Web, React), and Wasm (WebAssembly). Developers do not need to write different serialization implementations for each platform — the code remains the same. This is especially valuable in Kotlin Multiplatform Mobile (KMM) projects where shared code is used across Android and iOS.

How Compile-Time Code Generation Works

Code generation in kotlinx.serialization happens in three stages. In the first stage, the Kotlin compiler detects the @Serializable annotation on a class and passes it to the Kotlin Symbol Processing (KSP) plugin. In the second stage, KSP generates a serializer object that implements the KSerializer interface. In the third stage, the generated code is compiled together with the project’s source code. As a result, none of these stages execute during application runtime.

The generated serializer works directly with class fields through their getters and setters, without reflection. This means that fields with the private modifier are also serialized if they are annotated with @Serializable. The performance of this approach is close to manual serialization: for simple classes (5–10 fields), serialization time is 10–50 microseconds; for complex object graphs, up to 200 microseconds per 1000 objects.

To add the library to an Android or Kotlin/JVM project, you need to add the plugin and dependencies in build.gradle.kts. The org.jetbrains.kotlin.plugin.serialization plugin version must match the Kotlin version to activate code generation. The kotlinx-serialization-json library is added to the dependencies section with a version independent of the Kotlin version.

kotlin
// build.gradle.kts — adding kotlinx.serialization
plugins {
    val kotlinVersion = "2.1.0"
    kotlin("jvm") version kotlinVersion
    kotlin("plugin.serialization") version kotlinVersion
}

dependencies {
    // Main serialization module
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")

    // Additional formats
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:1.7.3")
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-cbor:1.7.3")
}

Basic Usage: JSON Serialization

JSON is the most popular format in kotlinx.serialization. To serialize an object, simply annotate the data class with @Serializable and call Json.encodeToString(). For deserialization, call Json.decodeFromString() with the type specified. The library automatically handles null fields, lists, nested objects, and enums. All class fields are required by default unless stated otherwise.

JSON configuration is done through the Json {} builder. You can pass ignoreUnknownKeys = true to skip unknown fields during deserialization, prettyPrint = true for formatted output, coerceInputValues = true to convert invalid values to defaults. You can also configure encodeDefaults (serialize fields with default values) and classDiscriminator (field name for polymorphic serialization).

kotlin
// JSON serialization and deserialization example
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration

@Serializable
data class Project(
    val name: String,
    val stars: Int,
    val isActive: Boolean = true,
    val languages: List<String> = emptyList()
)

fun main() {
    val project = Project(
        name = "kotlinx.serialization",
        stars = 7200,
        languages = listOf("Kotlin", "Java")
    )

    // JSON serialization with prettyPrint
    val json = Json { prettyPrint = true }
    val jsonString = json.encodeToString(project)
    println(jsonString)
    /*
    {
        "name": "kotlinx.serialization",
        "stars": 7200,
        "isActive": true,
        "languages": ["Kotlin", "Java"]
    }
    */

    // Deserialization from JSON
    val decoded = json.decodeFromString<Project>(jsonString)
    println(decoded.name)  // kotlinx.serialization
}

The example demonstrates the basic cycle of serialization and deserialization. A data class Project with the @Serializable annotation automatically gets encodeToString and decodeFromString. The isActive field has a default value of true — if this field is missing in JSON, the default is used. If unknown fields arrive in JSON without ignoreUnknownKeys = true, a SerializationException is thrown.

Polymorphic Serialization of Sealed Classes

Sealed classes are one of the most powerful use cases for kotlinx.serialization. The library supports polymorphic serialization for sealed class hierarchies without additional configuration: just annotate the sealed class and all its subclasses with @Serializable. During serialization, a “type” field is added (configurable via classDiscriminator), which determines the specific type during deserialization.

kotlin
// Polymorphic sealed class serialization
@Serializable
sealed class Response

@Serializable
data class Success(val data: String) : Response()

@Serializable
data class Error(val code: Int, val message: String) : Response()

fun main() {
    val json = Json { classDiscriminator = "result_type" }

    val responses: List<Response> = listOf(
        Success(data = "Data loaded"),
        Error(code = 404, message = "Not found")
    )

    val jsonString = json.encodeToString(responses)
    println(jsonString)
    /*
    [
        {"result_type":"Success","data":"Data loaded"},
        {"result_type":"Error","code":404,"message":"Not found"}
    ]
    */

    val decoded = json.decodeFromString<List<Response>>(jsonString)
    when (val first = decoded[0]) {
        is Success -> println("Success: ${first.data}")
        is Error -> println("Error: ${first.code}")
    }
}

Polymorphic sealed class serialization is especially useful in API clients where the server returns different response types. Without kotlinx.serialization, you would need to write a manual deserializer with a when statement on the discriminator field. With the library, this is done with a single annotation. classDiscriminator allows you to rename the marker field (default “type”) to any value expected by the server.

kotlinx.serialization Annotations: Full Overview

The library provides a set of annotations for fine-tuning serialization. The main one is @Serializable for a class. Additional ones: @SerialName to set a field’s name in JSON (if it differs from the Kotlin name), @Transient to exclude a field from serialization, @Required for a field that must be present in JSON, @EncodeDefault to force serialization of a field even with its default value.

AnnotationPurposeExample
@SerializableEnables serializer generation for a class@Serializable data class User
@SerialNameSets an alternative field name in the format@SerialName(“user_name”) val name: String
@TransientExcludes a field from serialization@Transient val cache: MutableMap
@RequiredField is mandatory in JSON during deserialization@Required val id: String
@EncodeDefaultSerializes the field even with its default value@EncodeDefault val type: Type = Type.A
@SerializerBinds a custom serializer to a class@Serializer(forClass = Date::class)

The @SerialName annotation is critical when working with APIs where field names are in snake_case but Kotlin style is camelCase. For example, the server sends “user_id”, while the Kotlin code uses userId. @SerialName(“user_id”) solves this problem without additional mappers. @Transient is useful for fields that should not be sent to the server — for example, temporary computed values or caches.

@Required as an Alternative to Nullable Fields

By default, all fields in kotlinx.serialization are required. If a field can be absent in JSON, you need to make it nullable (String?) or set a default value (val name: String = “”). However, there are situations where a field is non-nullable in Kotlin but might be missing in JSON due to API versioning. In this case, @Required throws a SerializationException when the field is absent, while a default value fills it without error.

Custom Serializers: KSerializer and Manual Control

KSerializer is the interface that all serializers in kotlinx.serialization implement. If the standard code generation does not suit your needs (for example, for working with Date, Bitmap, or a specific binary format), you can write your own serializer. To do this, implement the serialize() and deserialize() methods, and also provide a descriptor — a description of the structure for the format schema.

Custom serializers are connected in two ways: via the @Serializable(with = MySerializer::class) annotation to bind to a specific class, or globally via Json { serializersModule = ... } to bind to all instances of a type. The second approach is preferred for built-in types (Date, UUID) to avoid writing an annotation on every field.

kotlin
// Custom serializer for java.util.Date
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale

object DateSerializer : KSerializer<Date> {
    private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US)

    override val descriptor: SerialDescriptor =
        PrimitiveSerialDescriptor("Date", PrimitiveKind.STRING)

    override fun serialize(encoder: Encoder, value: Date) {
        encoder.encodeString(dateFormat.format(value))
    }

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

// Using a custom serializer
@Serializable
data class Event(
    val title: String,
    @Serializable(with = DateSerializer::class)
    val date: Date
)

fun main() {
    val json = Json { prettyPrint = true }
    val event = Event("Release", Date())
    println(json.encodeToString(event))
}

In the example, DateSerializer converts java.util.Date to an ISO 8601 string. Without a custom serializer, kotlinx.serialization cannot work with Date — it is a type not included in the standard Kotlin library. @Serializable(with = DateSerializer::class) on a specific field attaches the serializer only for that field. For global registration of all Dates, use Json { serializersModule = SerializersModule { contextual(DateSerializer) } }.

Serialization Formats: JSON, ProtoBuf, CBOR, HOCON

kotlinx.serialization is not limited to JSON. The library supports four built-in formats, each with its own module and configuration. JSON (kotlinx-serialization-json) is universal, human-readable, suitable for REST APIs. ProtoBuf (kotlinx-serialization-protobuf) is binary, compact, with a required schema, for high-load microservices. CBOR (kotlinx-serialization-cbor) is a binary JSON alternative, convenient for IoT and mobile devices with limited bandwidth. HOCON (kotlinx-serialization-hocon) is a configuration format compatible with TypeSafe Config.

FormatModuleTypeSchemaTypical Use
JSONkotlinx-serialization-jsonTextOptionalREST API, data storage
ProtoBufkotlinx-serialization-protobufBinaryRequired (.proto)Microservices, gRPC
CBORkotlinx-serialization-cborBinaryOptionalIoT, mobile devices
HOCONkotlinx-serialization-hoconTextOptionalConfiguration files

ProtoBuf requires defining a schema in .proto files, but kotlinx-serialization-protobuf generates Kotlin classes directly from @Serializable without .proto. This simplifies development: just annotate the data class and use ProtoBuf.encodeToByteArray(). CBOR is especially relevant for the Android framework when you need to transfer compact binary data via NFC or BLE. CBOR messages are on average 20–30% smaller than JSON for the same dataset.

Choosing a Format for Your Project

For REST APIs in a mobile app, JSON is the best choice — it can be debugged without additional tools, is readable in logs, and is compatible with any backend. If your application transfers large amounts of data between microservices (hundreds of megabytes), ProtoBuf provides a speed advantage of up to 5x due to binary encoding. For storing settings in files, use HOCON or JSON. For devices with strict traffic limits (IoT sensors), use CBOR.

Common Mistakes When Working with kotlinx.serialization

The first mistake is ignoring unknown keys during deserialization. If the server adds a new field and you have ignoreUnknownKeys = false, the application will crash with a SerializationException. This flag is off by default. Solution: always set Json { ignoreUnknownKeys = true } for production code to be resilient to API changes.

The second mistake is serializing internal or private fields in a data class. In a Kotlin data class, all fields in the primary constructor are serialized by default. If a field contains sensitive data (password, token), it must be marked with @Transient or moved out of the primary constructor. @Transient excludes the field from JSON entirely, but inside the constructor it may cause an error — it is better to define such fields in the class body with @Transient.

The third mistake is polymorphic serialization without sealed classes. If you use an open class instead of sealed, kotlinx.serialization requires explicit registration of all subclasses in serializersModule. Unlike sealed classes, where the compiler knows all subclasses, open classes allow arbitrary extension — the library cannot automatically determine all subtypes. Registration is done via Json { serializersModule = SerializersModule { polymorphic(Base::class) { subclass(Derived::class) } } }.

Library Versioning Mismatch

The kotlinx.serialization version must be compatible with the Kotlin version. JetBrains publishes a compatibility table: kotlinx-serialization 1.6.x is compatible with Kotlin 1.9.x, 1.7.x with Kotlin 2.0.x and 2.1.x. A version mismatch causes cryptic compilation errors like “Symbol ‘serializer’ is missing”. Always check the latest version on Maven Central or in the project’s GitHub repository.

Frequently Asked Questions

How does kotlinx.serialization differ from Gson and Moshi?

kotlinx.serialization uses compile-time code generation via KSP, while Gson and Moshi use runtime reflection. This provides a performance advantage (3–5x faster than Gson) and type safety. Gson serializes any field without annotation, which can lead to data leaks. kotlinx.serialization requires the explicit @Serializable annotation, making it safer. Moshi also supports codegen, but only for JVM and Android.

Does kotlinx.serialization support Kotlin Multiplatform?

Yes, kotlinx.serialization is an official JetBrains multiplatform library. It runs on Kotlin/JVM (Android, Backend), Kotlin/Native (iOS), Kotlin/JS (Web, React), and Kotlin/Wasm. The API is unified across all platforms: @Serializable + Json.encodeToString() works the same everywhere. For iOS, no additional setup is required — Kotlin/Native compiles the serialized code into a native binary.

How are null fields handled in JSON?

Nullable fields (String?) are deserialized as null if the value is missing or null in JSON. For non-nullable fields (String) without a default value, the absence of the field in JSON will throw a SerializationException. If you want null values not to appear in JSON, configure Json { encodeDefaults = false }. This excludes all fields equal to their default (including null for nullable types).

What if the server sends snake_case fields?

Use @SerialName(“snake_case_name”) on each field whose name differs from the Kotlin format. Alternatively, for Kotlin 2.0+, Json { namingStrategy = JsonNamingStrategy.SnakeCase } is available for automatic camelCase ↔ snake_case conversion. This setting applies to all fields at once. If partial customization is needed, combine @SerialName with the global strategy.

Can I serialize Kotlin Flow or coroutines?

No, Flow and coroutines are not directly serializable — they represent asynchronous execution, not data. To transfer data from a Flow, collect it into a collection via .toList() in a coroutine and serialize the collection. Similarly, you cannot serialize Job, Deferred, or Continuation. Only serialize data classes — model objects without behavioral logic.

Summary

  • kotlinx.serialization — compile-time serialization via @Serializable, no reflection, up to 5x faster than Gson
  • @Serializable, @SerialName, @Transient — key annotations for configuring field and class serialization
  • Json {} builder configures JSON: ignoreUnknownKeys, prettyPrint, coerceInputValues, encodeDefaults
  • Sealed classes and polymorphic serialization — seamless type hierarchy support without additional code
  • KSerializer — interface for custom serializers of non-standard types (Date, Bitmap, UUID)
  • Four formats: JSON, ProtoBuf, CBOR, HOCON — added as modules, unified API for all
  • Multiplatform — single codebase for JVM, Native, JS, and Wasm; critical for KMM and shared modules

We will develop a mobile application turnkey

IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.

Discuss the project

Read also