kotlinx.serialization adalah library serialisasi resmi Kotlin yang menghasilkan kode serializer pada waktu kompilasi. Berbeda dengan refleksi runtime, serializer yang dihasilkan kompilator cepat, aman terhadap perubahan tipe, dan tidak memerlukan reflect. Pada chapter ini kita akan memahami cara kerja @Serializable, format Json singleton, dan round-trip objek Kotlin ke JSON. Karena kotlinx.serialization nyata membutuhkan kompilator plugin, kita akan memodelkannya dengan anotasi marker dan serializer manual.
Anotasi @Serializable #
Anotasi @Serializable menandai data class sebagai kandidat serialisasi. Saat kompilator memproses class tersebut, ia menghasilkan NamaClass$$serializer — objek yang mengimplementasi KSerializer<T>. Serializer ini tahu urutan field, tipe, dan cara encode/decode tanpa refleksi runtime.
@Serializable
data class Produk(val id: Int, val nama: String, val harga: Int, val stok: Int)Class yang ditandai @Serializable otomatis dapat dipakai dengan Json.encodeToString(produk) dan Json.decodeFromString<Produk>(json). Tipe primitif (Int, String, Boolean, Long, Double) didukung langsung. Tipe kompleks seperti LocalDate memerlukan serializer kustom yang didaftarkan ke Json configuration.
Format Json Singleton #
Objek Json adalah entry point ke serializer. Konfigurasi umum meliputi ignoreUnknownKeys (lewati field JSON yang tidak ada di class), prettyPrint (indentasi untuk keterbacaan), encodeDefaults (sertakan field dengan nilai default). Konfigurasi ini diatur saat pembuatan instance Json { ... }.
val json = Json {
ignoreUnknownKeys = true
prettyPrint = true
encodeDefaults = true
}
val jsonString = json.encodeToString(produk)Pada kode produksi, satu instance Json biasanya dibuat sebagai singleton global agar konfigurasi konsisten.
Round-trip Serialize-Deserialize #
Round-trip berarti mengonversi objek ke JSON lalu kembali ke objek — dan memverifikasi kedua objek identik. Hal ini menguji bahwa serializer dan deserializer konsisten. Untuk data class Kotlin, round-trip umumnya aman karena field disimpan berdasarkan nama.
Demo kotlinx.serialization #
Karena kompilator plugin tidak tersedia di sandbox ini, kita akan memodelkan @Serializable sebagai anotasi marker dan menulis serializer manual yang meniru perilaku serializer yang dihasilkan kompilator. Pola ini memperlihatkan kontrak KSerializer<T> dengan jelas.
// ktor_serialization.kt
// Mendemonstrasikan kotlinx.serialization: @Serializable data class
// otomatis dikonversi ke JSON. Pada kode produksi, kompilator plugin
// kotlinx.serialization menghasilkan serializer. Di sini kita memakai
// anotasi marker + serializer manual berbasis refleksi sederhana.
// Mock @Serializable annotation (di produksi: kotlinx.serialization.Serializable)
@Target(AnnotationTarget.CLASS)
annotation class Serializable
// Antarmuka serializer yang dihasilkan oleh plugin
interface KSerializer<T> {
val descriptor: String
fun serialize(value: T): String
fun deserialize(text: String): T
}
@Serializable
data class Produk(val id: Int, val nama: String, val harga: Int, val stok: Int)
@Serializable
data class Pesanan(val id: Int, val produkId: Int, val jumlah: Int, val alamat: String)
// Serializer manual untuk Produk (mensimulasikan yang dihasilkan plugin)
object ProdukSerializer : KSerializer<Produk> {
override val descriptor = "Produk(id:Int,nama:String,harga:Int,stok:Int)"
override fun serialize(value: Produk): String =
"""{"id":${value.id},"nama":"${value.nama}","harga":${value.harga},"stok":${value.stok}}"""
override fun deserialize(text: String): Produk {
val id = Regex(""""id":(\d+)""").find(text)?.groupValues?.get(1)?.toInt() ?: 0
val nama = Regex(""""nama":"([^"]+)"""").find(text)?.groupValues?.get(1) ?: ""
val harga = Regex(""""harga":(\d+)""").find(text)?.groupValues?.get(1)?.toInt() ?: 0
val stok = Regex(""""stok":(\d+)""").find(text)?.groupValues?.get(1)?.toInt() ?: 0
return Produk(id, nama, harga, stok)
}
}
object PesananSerializer : KSerializer<Pesanan> {
override val descriptor = "Pesanan(id:Int,produkId:Int,jumlah:Int,alamat:String)"
override fun serialize(value: Pesanan): String =
"""{"id":${value.id},"produkId":${value.produkId},"jumlah":${value.jumlah},"alamat":"${value.alamat}"}"""
override fun deserialize(text: String): Pesanan {
val id = Regex(""""id":(\d+)""").find(text)?.groupValues?.get(1)?.toInt() ?: 0
val pid = Regex(""""produkId":(\d+)""").find(text)?.groupValues?.get(1)?.toInt() ?: 0
val jml = Regex(""""jumlah":(\d+)""").find(text)?.groupValues?.get(1)?.toInt() ?: 0
val alamat = Regex(""""alamat":"([^"]+)"""").find(text)?.groupValues?.get(1) ?: ""
return Pesanan(id, pid, jml, alamat)
}
}
// Format singleton (mirip Json.encodeToString / decodeFromString)
object Json {
private val serializers = mutableMapOf<String, KSerializer<*>>()
fun register(serializer: KSerializer<*>) {
val nama = serializer.descriptor.substringBefore("(")
serializers[nama] = serializer
}
@PublishedApi
internal fun findSerializer(nama: String): KSerializer<*> =
serializers[nama] ?: throw IllegalStateException("Tidak ada serializer untuk $nama")
inline fun <reified T> encodeToString(value: T): String {
val nama = T::class.simpleName ?: ""
@Suppress("UNCHECKED_CAST")
val serializer = findSerializer(nama) as KSerializer<T>
return serializer.serialize(value)
}
inline fun <reified T> decodeFromString(text: String): T {
val nama = T::class.simpleName ?: ""
@Suppress("UNCHECKED_CAST")
val serializer = findSerializer(nama) as KSerializer<T>
return serializer.deserialize(text)
}
}
fun main() {
Json.register(ProdukSerializer)
Json.register(PesananSerializer)
println("=== Serialize Produk ===")
val kopi = Produk(1, "Kopi Tubruk", 15000, 100)
val json1 = Json.encodeToString(kopi)
println(json1)
println("\n=== Deserialize Produk ===")
val parsed: Produk = Json.decodeFromString(json1)
println(parsed)
println("\n=== Serialize Pesanan ===")
val pesanan = Pesanan(10, 1, 3, "Jl. Mawar No. 1")
val json2 = Json.encodeToString(pesanan)
println(json2)
println("\n=== Deserialize Pesanan ===")
val parsedPesanan: Pesanan = Json.decodeFromString(json2)
println(parsedPesanan)
println("\n=== Round-trip verification ===")
println("Identik: ${kopi == parsed}")
println("Identik: ${pesanan == parsedPesanan}")
}Output yang Dihasilkan:
=== Serialize Produk ===
{"id":1,"nama":"Kopi Tubruk","harga":15000,"stok":100}
=== Deserialize Produk ===
Produk(id=1, nama=Kopi Tubruk, harga=15000, stok=100)
=== Serialize Pesanan ===
{"id":10,"produkId":1,"jumlah":3,"alamat":"Jl. Mawar No. 1"}
=== Deserialize Pesanan ===
Pesanan(id=10, produkId=1, jumlah=3, alamat=Jl. Mawar No. 1)
=== Round-trip verification ===
Identik: true
Identik: trueMari kita bedah bagian penting kode di atas:
@Target(AnnotationTarget.CLASS) annotation class Serializable: anotasi marker tanpa parameter. Pada produksi,kotlinx.serialization.Serializablejuga sebuah anotasi — tetapi memicu kompilator plugin untuk menghasilkan serializer.interface KSerializer<T> { val descriptor; fun serialize; fun deserialize }: kontrak serializer dengandescriptoryang mendeskripsikan struktur. Descriptor dipakai untuk debugging dan untuk format yang membutuhkan schema (mis. ProtoBuf).inline fun <reified T> encodeToString(value: T): function reified agarT::class.simpleNamedapat diakses pada runtime. Inilah trik yang membuat API terlihat sepertiJson.encodeToString(produk)tanpa argumen tipe eksplisit.Json.register(ProdukSerializer): pendaftaran serializer manual. Pada produksi, kompilator plugin mendaftarkan serializer otomatis — pengembang tidak perlu memanggilregistersama sekali.println("Identik: ${kopi == parsed}"): verifikasi round-trip. Data class Kotlin menghasilkanequalsberbasis seluruh field, sehingga perbandingan ini akurat.
Konfigurasi Penting #
Beberapa konfigurasi Json yang sering dipakai di backend:
| Opsi | Perilaku |
|---|---|
ignoreUnknownKeys = true | Lewati field JSON yang tidak ada di class |
prettyPrint = true | Indentasi untuk keterbacaan |
encodeDefaults = true | Sertakan field dengan nilai default |
explicitNulls = false | Hilangkan field null dari output |
coerceInputValues = true | Coerce nilai invalid ke default |
Konfigurasi ignoreUnknownKeys = true sangat penting untuk kompatibilitas versi — klien lama yang tidak tahu field baru tidak akan rusak.
Referensi Lanjutan #
- Ktor serialization
— panduan resmi konversi objek Kotlin ke JSON lewat
kotlinx.serializationdi Ktor - Ktor content negotiation
— panduan resmi plugin
ContentNegotiationyang menghubungkan body HTTP dengan serializer
Selanjutnya #
Setelah memahami serialisasi, langkah berikutnya adalah memvalidasi input yang sudah ter-deserialize. Chapter Ktor Validation akan membahas validator kustom, pengumpulan error multi-field, dan format respons error mengikuti RFC 7807 Problem Details.