Kotlin Serialization Explained
kotlinx.serialization generates serializers at compile time via a compiler plugin — Charlie's Order ↔ JSON conversion is zero-reflection, type-safe, and faster than Jackson. @SerialName handles field mapping in one line; Json { ignoreUnknownKeys = true } configures fault tolerance in one line.
1. What You'll Learn
@Serializablecompiler plugin generating serializers at compile time- JSON encoding/decoding:
encodeToString/decodeFromString<T> - Optional fields and defaults:
@SerialName/@Required - Multi-format support: JSON / ProtoBuf / CBOR / HOCON
- Charlie in action: Order ↔ JSON + field mapping + fault-tolerant configuration
2. A Real Developer's Story
(1) Pain Point: Reflection-Based Serialization Runtime Bombs
Bob used Jackson's reflection mode to serialize Order objects. During a refactor, orderId was renamed to id — JSON deserialization silently failed (field name mismatch), and 500 order records were lost.
(2) Compile-Time Serialization Solution
KOTLIN
// Jackson: reflection-based, errors at runtime
@JsonAlias("order_id") // Easy to forget
data class Order(val orderId: String, ...)
// kotlinx.serialization: compile-time, errors at compile time
@Serializable
data class Order(
@SerialName("order_id") val id: String, // Compiler verifies!
val total: Double
)
Compile-time serializer generation — field name changes become compile errors, not runtime data loss.
3. @Serializable and the Compiler Plugin
(1) Gradle Configuration
KOTLIN
// build.gradle.kts
plugins {
kotlin("jvm") version "1.9.22"
kotlin("plugin.serialization") version "1.9.22" // Serialization compiler plugin
}
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
}
(2) Basic Serialization
KOTLIN
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
@Serializable
data class Order(val id: String, val total: Double, val status: String)
// Serialize: Object -> JSON String
val order = Order("ORD-001", 299.99, "CONFIRMED")
val json = Json.encodeToString(order)
// {"id":"ORD-001","total":299.99,"status":"CONFIRMED"}
// Deserialize: JSON String -> Object
val decoded = Json.decodeFromString<Order>(json)
// Order(id=ORD-001, total=299.99, status=CONFIRMED)
(3) Reflection vs Compile-Time Serialization
| Dimension | Jackson (Reflection) | kotlinx.serialization |
|---|---|---|
| Mechanism | Runtime reflection | Compile-time code generation |
| Safety | Runtime errors | Compile-time errors |
| Performance | Slower | Faster (no reflection overhead) |
| ProGuard | Requires keep rules | Not needed |
| Multiplatform | JVM only | JVM / Native / JS |
4. JSON Encoding and Decoding
(1) Json Configuration
KOTLIN
// Default: strict mode
val strictJson = Json // Fails on unknown keys
// Lenient: ignore unknown keys (API versioning)
val lenientJson = Json {
ignoreUnknownKeys = true // Ignore fields not in class
isLenient = true // Accept malformed JSON
encodeDefaults = true // Include fields with default values
prettyPrint = true // Pretty print output
prettyPrintIndent = " " // Indentation
coerceInputValues = true // Use default for null on non-null
}
val json = lenientJson.encodeToString(order)
(2) Json Configuration Options
| Option | Default | Description |
|---|---|---|
ignoreUnknownKeys |
false | Ignore JSON fields not present in the class |
isLenient |
false | Lenient parsing (accept non-standard JSON) |
encodeDefaults |
false | Encode fields with default values |
prettyPrint |
false | Formatted output |
coerceInputValues |
false | Use default value when non-null field receives null |
5. Field Mapping and Defaults
(1) @SerialName Field Mapping
KOTLIN
@Serializable
data class Order(
@SerialName("order_id") val id: String, // JSON: order_id
@SerialName("order_total") val total: Double, // JSON: order_total
val status: String = "PENDING" // JSON: status (same name)
)
// JSON: {"order_id":"ORD-001","order_total":299.99,"status":"CONFIRMED"}
(2) Optional Fields and Defaults
KOTLIN
@Serializable
data class Order(
@SerialName("order_id") val id: String,
@SerialName("order_total") val total: Double,
val status: String = "PENDING", // Optional with default
val customer: String? = null, // Nullable optional
@SerialName("tax_rate") val taxRate: Double = 0.08
)
// Missing optional fields use defaults
val json = """{"order_id":"ORD-001","order_total":299.99}"""
val order = Json.decodeFromString<Order>(json)
// Order(id=ORD-001, total=299.99, status=PENDING, customer=null, taxRate=0.08)
(3) @Required to Force Fields
KOTLIN
@Serializable
data class Order(
@Required val id: String, // MUST be present in JSON
@Required val total: Double // MUST be present in JSON
)
// Missing 'id' or 'total' -> SerializationException
6. Multi-Format Support
KOTLIN
// ProtoBuf
import kotlinx.serialization.protobuf.ProtoBuf
val protoBytes = ProtoBuf.encodeToByteArray(order)
val fromProto = ProtoBuf.decodeFromByteArray<Order>(protoBytes)
// CBOR
import kotlinx.serialization.cbor.Cbor
val cborBytes = Cbor.encodeToByteArray(order)
// HOCON (configuration format)
import kotlinx.serialization.hocon.Hocon
(1) Format Comparison
| Format | Readability | Size | Speed | Use Case |
|---|---|---|---|---|
| JSON | High | Large | Medium | API communication |
| ProtoBuf | Low (binary) | Small | Fast | High-performance RPC |
| CBOR | Low (binary) | Medium | Fast | IoT / embedded |
| HOCON | High | Medium | Medium | Configuration files |
7. Serialization Encode/Decode Flow
sequenceDiagram
participant Obj as Order Object
participant Ser as Serializer
participant JSON as JSON String
Note over Obj,Ser: Encode
Obj->>Ser: @Serializable properties
Ser->>JSON: encodeToString()
Note over JSON: {"order_id":"ORD-001",...}
Note over Ser,Obj: Decode
JSON->>Ser: decodeFromString<Order>(json)
Ser->>Obj: @Serializable constructor
Note over Obj: Order(id=ORD-001,...)
8. Complete Example: OrderProcessor JSON Serialization
KOTLIN
// ============================================
// OrderProcessor - JSON Serialization
// Feature: Order <-> JSON with field mapping
// ============================================
import kotlinx.serialization.*
import kotlinx.serialization.json.*
@Serializable
data class Address(
val street: String,
val city: String,
val country: String
)
@Serializable
data class OrderItem(
val sku: String,
val quantity: Int,
@SerialName("unit_price") val unitPrice: Double
) {
val subtotal: Double get() = quantity * unitPrice
}
@Serializable
data class Order(
@SerialName("order_id") val id: String,
@SerialName("order_total") val total: Double,
val status: String = "PENDING",
val customer: String? = null,
val items: List<OrderItem> = emptyList(),
val address: Address? = null,
@SerialName("tax_rate") val taxRate: Double = 0.08,
@SerialName("created_at") val createdAt: String = "2026-01-01T00:00:00Z"
)
val orderJson = Json {
ignoreUnknownKeys = true
encodeDefaults = true
prettyPrint = true
prettyPrintIndent = " "
}
fun main() {
// Create order
val order = Order(
id = "ORD-001",
total = 299.99,
status = "CONFIRMED",
customer = "Alice",
items = listOf(
OrderItem("SKU-WIDGET", 3, 9.99),
OrderItem("SKU-GADGET", 1, 149.99)
),
address = Address("123 Main St", "New York", "US"),
createdAt = "2026-07-13T10:30:00Z"
)
// Serialize: Order -> JSON
println("=== Serialize ===")
val jsonString = orderJson.encodeToString(order)
println(jsonString)
// Deserialize: JSON -> Order
println("\n=== Deserialize ===")
val decoded = orderJson.decodeFromString<Order>(jsonString)
println("Order: ${decoded.id}, Total: \$${decoded.total} USD")
println("Items: ${decoded.items.map { "${it.sku} x${it.quantity}" }}")
// Handle unknown keys (API versioning)
println("\n=== API Versioning ===")
val jsonWithExtraFields = """
{
"order_id": "ORD-002",
"order_total": 1500.00,
"status": "SHIPPED",
"unknown_field": "this is fine",
"new_api_version": 2
}
""".trimIndent()
val decodedWithExtra = orderJson.decodeFromString<Order>(jsonWithExtraFields)
println("Decoded with extra fields: ${decodedWithExtra.id}")
// Minimal JSON (required fields only)
println("\n=== Minimal JSON ===")
val minimalJson = """{"order_id":"ORD-003","order_total":45.50}"""
val minimal = orderJson.decodeFromString<Order>(minimalJson)
println("Minimal: ${minimal.id}, Status: ${minimal.status}, Customer: ${minimal.customer}")
}
Output:
TEXT
=== Serialize ===
{
"order_id": "ORD-001",
"order_total": 299.99,
"status": "CONFIRMED",
"customer": "Alice",
"items": [
{
"sku": "SKU-WIDGET",
"quantity": 3,
"unit_price": 9.99
},
{
"sku": "SKU-GADGET",
"quantity": 1,
"unit_price": 149.99
}
],
"address": {
"street": "123 Main St",
"city": "New York",
"country": "US"
},
"tax_rate": 0.08,
"created_at": "2026-07-13T10:30:00Z"
}
=== Deserialize ===
Order: ORD-001, Total: $299.99 USD
Items: [SKU-WIDGET x3, SKU-GADGET x1]
=== API Versioning ===
Decoded with extra fields: ORD-002
=== Minimal JSON ===
Minimal: ORD-003, Status: PENDING, Customer: null
❓ FAQ
Q Can kotlinx.serialization be used alongside Jackson?
A Yes, but it's not recommended. The two serialization mechanisms differ. During migration, you can use
@JsonAlias as a bridge; for new projects, go directly with kotlinx.serialization.Q Can sealed classes be serialized?
A Yes.
@Serializable sealed classes automatically include a type discriminator ("type":"SubClassName"), and the correct subclass is chosen during deserialization.Q How do I customize serialization logic?
A Specify a custom serializer with
@Serializable(with = CustomSerializer::class), or implement the KSerializer<T> interface to manually control encoding/decoding.Q Does serialization support generics?
A Yes, but you need to annotate the generic class with
@Serializable. decodeFromString requires explicit type parameters.Q Why is the compiler plugin needed?
A Kotlin's compiler plugin auto-generates serializer code for
@Serializable classes at compile time, avoiding runtime reflection. This is the foundation of its performance and safety.Q What's the difference between @SerialName and @JsonProperty?
A Functionally the same (JSON field name mapping), but @SerialName is kotlinx.serialization's annotation processed at compile time; @JsonProperty is Jackson's annotation processed at runtime via reflection.
📖 Summary
@Serializable+ compiler plugin auto-generates serializers — zero reflectionJson.encodeToString/Json.decodeFromString<T>for encoding and decoding@SerialNamemaps JSON field names, decoupled from Kotlin property names- Default values = optional fields;
@Requiredforces field presence ignoreUnknownKeys = trueenables API version compatibility- Multi-format support: JSON / ProtoBuf / CBOR / HOCON
📝 Exercises
- Beginner (⭐): Add
@SerializabletoOrder, serialize to JSON, and deserialize back. Hint:@Serializable data class Order(...) - Intermediate (⭐⭐): Use
@SerialNameto map Order field names (order_id,order_total), and configureJson { ignoreUnknownKeys = true }for API evolution. Hint: Reference Section 5 - Challenge (⭐⭐⭐): Implement a custom serializer that flattens
Orderitems into a flat JSON format (expanding items toitem_1_sku,item_1_qtyformat). Hint: ImplementKSerializer<Order>



