Serialisasi manual seperti pada chapter sebelumnya cocok untuk belajar, tetapi tidak praktis untuk proyek produksi. Kita harus menulis serializer dan deserializer untuk setiap model—dan menjaga keduanya tetap sinkron saat model berubah. Library kotlinx.serialization dari JetBrains menyelesaikan ini: anotasi @Serializable memicu compiler plugin menghasilkan serializer otomatis saat build. Tidak ada reflection runtime untuk serialization field, sehingga aman untuk Kotlin/Native dan Kotlin/JS. Pada chapter ini kita akan memahami arsitektur kotlinx.serialization, melihat simulasi serializer generator, dan bagaimana anotasi @SerialName, @Transient, @Optional mengontrol perilaku serialization.

Arsitektur: Annotation + Compiler Plugin #

kotlinx.serialization terdiri dari dua bagian. Pertama, anotasi @Serializable, @SerialName, @Transient yang dipakai di source code. Kedua, compiler plugin org.jetbrains.kotlin.plugin.serialization yang memindai anotasi saat kompilasi dan menghasilkan class $$serializer untuk setiap model. Saat runtime, Json.encodeToString(obj) mencari serializer yang sudah di-generate dan memanggil method serialize().

kotlin build.gradle.kts: plugin dan dependency
plugins {
    kotlin("jvm") version "2.0.21"
    kotlin("plugin.serialization") version "2.0.21"
}

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
}
kotlin Penggunaan dasar
import kotlinx.serialization.*
import kotlinx.serialization.json.*

@Serializable
data class Pengguna(val id: Long, val nama: String)

fun main() {
    val u = Pengguna(1, "Budi")
    val json = Json.encodeToString(u)
    // {"id":1,"nama":"Budi"}

    val loaded = Json.decodeFromString<Pengguna>(json)
    // Pengguna(id=1, nama=Budi)
}

Anotasi yang Sering Dipakai #

AnotasiTujuan
@SerializableMenandai class untuk generator serializer
@SerialName("name")Mengganti nama field di JSON (mis. camelCase → snake_case)
@TransientField tidak di-serialize; harus punya default value
@OptionalField opsional di JSON; diisi default jika tidak ada
@RequiredField wajib ada di JSON; error jika tidak ada

@SerialName berguna saat nama field Kotlin tidak cocok dengan kontrak API. Mis. field Kotlin namaLengkap dipetakan ke full_name di JSON yang dipakai backend Python dengan konvensi snake_case.

Simulasi Serializer Generator #

Karena kotlinx.serialization memerlukan plugin compiler yang tidak terpasang di lingkungan ini, demo berikut mensimulasikan perilaku plugin. @KSerializable adalah marker anotasi buatan; PenggunaSerializer adalah class yang akan di-generate plugin pada proyek nyata.

kotlin
// kotlinx_serialization.kt
// kotlinx.serialization: @Serializable annotation + compiler plugin.
// Demo: simulasi serializer generator + Json.encodeToString / decodeFromString.

// --- Marker anotasi (simulasi @Serializable dari kotlinx.serialization) ---

@Target(AnnotationTarget.CLASS)
annotation class KSerializable

@Target(AnnotationTarget.PROPERTY)
annotation class SerialName(val name: String)

@Target(AnnotationTarget.PROPERTY)
annotation class Transient

@Target(AnnotationTarget.PROPERTY)
annotation class Optional

// Model dengan anotasi (simulasi @Serializable)
@KSerializable
data class Pengguna(
    @property:SerialName("user_id") val id: Long,
    @property:SerialName("full_name") val nama: String,
    @property:SerialName("email_address") val email: String,
    @property:Transient val passwordHash: String = "",
    @property:Optional val umur: Int? = null
)

// --- Compiler-generated serializer (simulasi) ---
// Pada proyek nyata, plugin kotlinx.serialization menghasilkan class
// Pengguna$$serializer otomatis saat compile.

interface KSerializer<T> {
    fun serialize(obj: T): String
    fun deserialize(text: String): T
}

class PenggunaSerializer : KSerializer<Pengguna> {
    override fun serialize(obj: Pengguna): String {
        // Generator membaca anotasi dan menulis field sesuai @SerialName
        val parts = mutableListOf<String>()
        parts.add("\"user_id\":${obj.id}")
        parts.add("\"full_name\":\"${escape(obj.nama)}\"")
        parts.add("\"email_address\":\"${escape(obj.email)}\"")
        // @Transient: skip (tidak ditulis ke JSON)
        // @Optional: tulis hanya jika tidak null
        if (obj.umur != null) parts.add("\"umur\":${obj.umur}")
        return "{${parts.joinToString(",")}}"
    }

    override fun deserialize(text: String): Pengguna {
        // Parser sederhana untuk JSON yang dihasilkan serialize()
        val id = Regex("\"user_id\"\\s*:\\s*(\\d+)").find(text)?.groupValues?.get(1)?.toLong()
            ?: throw IllegalArgumentException("user_id wajib")
        val nama = Regex("\"full_name\"\\s*:\\s*\"([^\"]+)\"").find(text)?.groupValues?.get(1)
            ?: throw IllegalArgumentException("full_name wajib")
        val email = Regex("\"email_address\"\\s*:\\s*\"([^\"]+)\"").find(text)?.groupValues?.get(1)
            ?: throw IllegalArgumentException("email_address wajib")
        val umur = Regex("\"umur\"\\s*:\\s*(\\d+)").find(text)?.groupValues?.get(1)?.toInt()
        return Pengguna(id, nama, email, passwordHash = "", umur = umur)
    }

    private fun escape(s: String): String =
        s.replace("\\", "\\\\").replace("\"", "\\\"")
}

// --- Json object (simulasi kotlinx.serialization.json.Json) ---
object Json {
    val serializers: MutableMap<Class<*>, KSerializer<*>> = mutableMapOf()

    init {
        // Pada proyek nyata, lookup dilakukan via SerializerRegistry yang
        // dihasilkan compiler. Di sini kita daftarkan manual.
        serializers[Pengguna::class.java] = PenggunaSerializer()
    }

    @Suppress("UNCHECKED_CAST")
    inline fun <reified T> encodeToString(value: T): String {
        val serializer = serializers[T::class.java]
            ?: throw IllegalStateException("Tidak ada serializer untuk ${T::class.java}. Tambahkan @Serializable.")
        return (serializer as KSerializer<T>).serialize(value)
    }

    @Suppress("UNCHECKED_CAST")
    inline fun <reified T> decodeFromString(text: String): T {
        val serializer = serializers[T::class.java]
            ?: throw IllegalStateException("Tidak ada serializer untuk ${T::class.java}")
        return (serializer as KSerializer<T>).deserialize(text)
    }
}

// --- Demo ---

fun main() {
    println("=== kotlinx.serialization (simulasi) ===\n")

    val pengguna = Pengguna(
        id = 1001L,
        nama = "Budi Santoso",
        email = "[email protected]",
        passwordHash = "secret-hash-tidak-terlihat",
        umur = 28
    )

    println("Object asli  : $pengguna")
    println()

    println("--- encodeToString ---")
    val json = Json.encodeToString(pengguna)
    println("JSON output  : $json")
    println()

    println("--- Perhatikan anotasi yang dipatuhi ---")
    println("user_id       = ${extract(json, "user_id")}  (dari @SerialName)")
    println("full_name     = ${extract(json, "full_name")}  (dari @SerialName)")
    println("email_address = ${extract(json, "email_address")}  (dari @SerialName)")
    println("password_hash = ${extract(json, "password_hash") ?: "(tidak ada)"}  (@Transient)")
    println("umur          = ${extract(json, "umur") ?: "(tidak ada)"}  (@Optional, tidak null)")
    println()

    println("--- decodeFromString ---")
    val jsonLengkap = """{"user_id":1002,"full_name":"Siti Aprilia","email_address":"[email protected]","umur":31}"""
    val restored = Json.decodeFromString<Pengguna>(jsonLengkap)
    println("JSON input   : $jsonLengkap")
    println("Hasil decode : $restored")
    println()

    println("--- Decode dengan field @Optional hilang ---")
    val jsonTanpaUmur = """{"user_id":1003,"full_name":"Dewi Lestari","email_address":"[email protected]"}"""
    val restored2 = Json.decodeFromString<Pengguna>(jsonTanpaUmur)
    println("JSON input   : $jsonTanpaUmur")
    println("Hasil decode : $restored2")
    println("umur null karena @Optional dan field tidak ada di JSON")
    println()

    println("--- Error: field wajib hilang ---")
    try {
        val jsonKurang = """{"full_name":"Budi","email_address":"[email protected]"}"""
        Json.decodeFromString<Pengguna>(jsonKurang)
    } catch (e: IllegalArgumentException) {
        println("Ditolak: ${e.message}")
    }
    println()

    println("--- Ringkasan: yang dilakukan compiler plugin ---")
    println("1. Saat @Serializable ditemukan, plugin generate Pengguna\$\$serializer")
    println("2. Serializer membaca anotasi @SerialName, @Transient, @Optional")
    println("3. Json.encodeToString(x) memanggil serializer via reflection")
    println("4. Json.decodeFromString<T>(text) memanggil serializer.deserialize()")
    println("5. Tidak ada reflection di runtime untuk serialization field")
    println("6. Compile-time safety: field wajib divalidasi saat kompilasi")
}

fun extract(json: String, key: String): String? {
    val pattern = Regex("\"$key\"\\s*:\\s*\"?([^\",}]+)\"?")
    return pattern.find(json)?.groupValues?.get(1)
}

Output yang Dihasilkan:

text
=== kotlinx.serialization (simulasi) ===

Object asli  : Pengguna(id=1001, nama=Budi Santoso, [email protected], passwordHash=secret-hash-tidak-terlihat, umur=28)

--- encodeToString ---
JSON output  : {"user_id":1001,"full_name":"Budi Santoso","email_address":"[email protected]","umur":28}

--- Perhatikan anotasi yang dipatuhi ---
user_id       = 1001  (dari @SerialName)
full_name     = Budi Santoso  (dari @SerialName)
email_address = [email protected]  (dari @SerialName)
password_hash = (tidak ada)  (@Transient)
umur          = 28  (@Optional, tidak null)

--- decodeFromString ---
JSON input   : {"user_id":1002,"full_name":"Siti Aprilia","email_address":"[email protected]","umur":31}
Hasil decode : Pengguna(id=1002, nama=Siti Aprilia, [email protected], passwordHash=, umur=31)

--- Decode dengan field @Optional hilang ---
JSON input   : {"user_id":1003,"full_name":"Dewi Lestari","email_address":"[email protected]"}
Hasil decode : Pengguna(id=1003, nama=Dewi Lestari, [email protected], passwordHash=, umur=null)
umur null karena @Optional dan field tidak ada di JSON

--- Error: field wajib hilang ---
Ditolak: user_id wajib

--- Ringkasan: yang dilakukan compiler plugin ---
1. Saat @Serializable ditemukan, plugin generate Pengguna$$serializer
2. Serializer membaca anotasi @SerialName, @Transient, @Optional
3. Json.encodeToString(x) memanggil serializer via reflection
4. Json.decodeFromString<T>(text) memanggil serializer.deserialize()
5. Tidak ada reflection di runtime untuk serialization field
6. Compile-time safety: field wajib divalidasi saat kompilasi

Mari kita bedah bagian kunci dari kode di atas:

  1. annotation class KSerializable: marker anotasi yang mensimulasikan @Serializable dari library. Saat dipasang plugin kotlinx.serialization, anotasi asli memicu compiler generate serializer.
  2. annotation class SerialName, Transient, Optional: tiga anotasi pendamping. @SerialName mengganti nama field di JSON; @Transient mengecualikan field; @Optional membuat field opsional dengan default value.
  3. data class Pengguna: model dengan lima field. id, nama, email memakai @SerialName untuk pemetaan snake_case. passwordHash memakai @Transient agar tidak bocor ke JSON. umur memakai @Optional dan nullable—di-serialize hanya jika tidak null.
  4. interface KSerializer<T>: kontrak serializer dengan serialize(obj) dan deserialize(text). Pada library nyata, interface ini punya method serialize(encoder: Encoder) dan deserialize(decoder: Decoder) yang abstrak terhadap format (JSON, ProtoBuf, dll).
  5. PenggunaSerializer: implementasi yang akan di-generate compiler. Membaca anotasi saat serialize—@Transient field dilewati, @Optional field hanya ditulis jika tidak null. Saat deserialize, field wajib divalidasi—user_id hilang menyebabkan IllegalArgumentException.
  6. object Json: simulasi kotlinx.serialization.json.Json. Method encodeToString<T>(value) dan decodeFromString<T>(text) menggunakan reified type parameter untuk lookup serializer di registry.
  7. Output menunjukkan tiga skenario: encode (password_hash tidak muncul di JSON karena @Transient), decode lengkap (semua field terisi), decode tanpa umur (field @Optional diisi null). Error dilempar saat field wajib hilang.

Compile-time Safety #

Keuntungan utama kotlinx.serialization dibanding library berbasis reflection seperti Gson: compile-time safety. Saat kita menghapus field dari data class, compiler otomatis menghapus dari serializer yang di-generate. Saat kita menambah field tanpa default value, compiler memperingatkan bahwa field wajib ada di JSON.

kotlin Compile error jika field wajib tanpa default
@Serializable
data class Pengguna(
    val id: Long,
    val nama: String,
    val telepon: String  // error: field wajib tanpa default
                          // data JSON lama tidak akan punya field ini
)

Pada proyek dengan banyak model, compile-time safety ini menyelamatkan tim dari bug runtime yang muncul karena lupa memperbarui serializer.

Referensi Lanjutan #

Penutup #

kotlinx.serialization menggabungkan kemudahan anotasi dengan kecepatan kode yang di-generate. Compiler plugin menghasilkan serializer otomatis; runtime tidak memakai reflection untuk serialization field. Anotasi @SerialName, @Transient, @Optional mengontrol pemetaan antara model Kotlin dan format JSON. Chapter berikutnya akan masuk lebih dalam ke JSON serialization—konfigurasi Json, penanganan field nullable, dan kasus umum seperti polymorphism.