kotlinx.serialization menghasilkan serializer otomatis untuk tipe dasar—String, Int, Boolean, Long, dan data class yang berisi tipe tersebut. Namun, tipe kompleks seperti LocalDate, BigDecimal, atau UUID tidak didukung bawaan. Untuk tipe tersebut, kita menulis KSerializer<T> sendiri. Custom serializer juga berguna saat format JSON berbeda dari representasi Kotlin—mis. Color(255, 0, 0) ingin di-serialize sebagai "#FF0000". Pada chapter ini kita akan melihat pola penulisan KSerializer<T>, tiga contoh nyata (LocalDate, BigDecimal, Color), dan kapan harus memilih custom serializer versus alternatif lain.
Interface KSerializer<T> #
KSerializer<T> adalah interface inti yang harus diimplementasi custom serializer. Tiga anggota utama: descriptor yang mendeskripsikan struktur tipe, serialize(obj) yang mengubah object menjadi format target, dan deserialize(text) yang melakukan kebalikannya.
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*
object LocalDateSerializer : KSerializer<LocalDate> {
override val descriptor = PrimitiveSerialDescriptor(
"LocalDate", PrimitiveKind.STRING
)
override fun serialize(encoder: Encoder, value: LocalDate) {
encoder.encodeString(value.toString()) // ISO-8601
}
override fun deserialize(decoder: Decoder): LocalDate {
return LocalDate.parse(decoder.decodeString())
}
}Encoder dan Decoder adalah abstraksi atas format—JSON, ProtoBuf, atau format lain. Dengan menulis satu serializer, kita dapat memakainya untuk semua format yang didukung library.
Tiga Contoh Custom Serializer #
Demo berikut menunjukkan tiga custom serializer yang sering dibutuhkan di proyek nyata. Karena kotlinx.serialization memerlukan compiler plugin, demo mensimulasikan interface KSerializer<T> dengan serialize(obj): String dan deserialize(text): T—pola yang sama dengan library nyata.
// custom_serializers.kt
// Custom serializer: KSerializer<T> untuk tipe kompleks.
// Demo: LocalDate, BigDecimal, Color, Range via custom serializer.
import java.math.BigDecimal
import java.math.RoundingMode
import java.time.LocalDate
// --- Wrapper untuk anotasi (simulasi) ---
@Target(AnnotationTarget.CLASS)
annotation class KSerializable
@Target(AnnotationTarget.PROPERTY)
annotation class WithSerializer(val serializer: String)
// --- Interface KSerializer<T> ---
interface KSerializer<T> {
val descriptor: String
fun serialize(obj: T): String
fun deserialize(text: String): T
}
// --- Custom serializer 1: LocalDate <-> "yyyy-MM-dd" ---
object LocalDateSerializer : KSerializer<LocalDate> {
override val descriptor: String = "LocalDate(String)"
override fun serialize(obj: LocalDate): String {
// LocalDate.toString() sudah menghasilkan ISO-8601 yyyy-MM-dd
return "\"${obj.toString()}\""
}
override fun deserialize(text: String): LocalDate {
// Hapus tanda kutip jika ada
val clean = text.trim().trim('"')
return LocalDate.parse(clean)
}
}
// --- Custom serializer 2: BigDecimal <-> string presisi tinggi ---
object BigDecimalSerializer : KSerializer<BigDecimal> {
override val descriptor: String = "BigDecimal(String)"
override fun serialize(obj: BigDecimal): String {
// Selalu serialize sebagai string untuk menjaga presisi
// Mengapa? Number JSON bisa hilang presisi pada parser double
return "\"${obj.toPlainString()}\""
}
override fun deserialize(text: String): BigDecimal {
val clean = text.trim().trim('"')
return BigDecimal(clean)
}
}
// --- Custom serializer 3: Color (RGB) <-> hex string ---
data class Color(val r: Int, val g: Int, val b: Int) {
fun toHex(): String = String.format("#%02X%02X%02X", r, g, b)
companion object {
fun fromHex(hex: String): Color {
val h = hex.removePrefix("#")
require(h.length == 6) { "hex harus 6 karakter: $hex" }
val r = h.substring(0, 2).toInt(16)
val g = h.substring(2, 4).toInt(16)
val b = h.substring(4, 6).toInt(16)
return Color(r, g, b)
}
}
}
object ColorSerializer : KSerializer<Color> {
override val descriptor: String = "Color(hex)"
override fun serialize(obj: Color): String = "\"${obj.toHex()}\""
override fun deserialize(text: String): Color {
val hex = text.trim().trim('"')
return Color.fromHex(hex)
}
}
// --- Model yang memakai custom serializer ---
@KSerializable
data class Transaksi(
val id: Long,
val tanggal: LocalDate,
val jumlah: BigDecimal,
val warna: Color
)
// --- Serializer untuk Transaksi ---
class TransaksiSerializer {
fun serialize(t: Transaksi): String {
val parts = mutableListOf<String>()
parts.add("\"id\":${t.id}")
parts.add("\"tanggal\":${LocalDateSerializer.serialize(t.tanggal)}")
parts.add("\"jumlah\":${BigDecimalSerializer.serialize(t.jumlah)}")
parts.add("\"warna\":${ColorSerializer.serialize(t.warna)}")
return "{${parts.joinToString(",")}}"
}
fun deserialize(text: String): Transaksi {
val id = extractLong(text, "id") ?: error("id wajib")
val tanggalText = extractRaw(text, "tanggal") ?: error("tanggal wajib")
val jumlahText = extractRaw(text, "jumlah") ?: error("jumlah wajib")
val warnaText = extractRaw(text, "warna") ?: error("warna wajib")
return Transaksi(
id = id,
tanggal = LocalDateSerializer.deserialize(tanggalText),
jumlah = BigDecimalSerializer.deserialize(jumlahText),
warna = ColorSerializer.deserialize(warnaText)
)
}
}
// --- Helper extraction ---
fun extractLong(json: String, key: String): Long? =
Regex("\"$key\"\\s*:\\s*(-?\\d+)").find(json)?.groupValues?.get(1)?.toLong()
fun extractRaw(json: String, key: String): String? {
val m = Regex("\"$key\"\\s*:\\s*(\"[^\"]*\"|\\d+)").find(json)
return m?.groupValues?.get(1)
}
// --- Demo ---
fun main() {
println("=== Custom Serializers ===\n")
println("--- 1. LocalDate <-> ISO-8601 string ---")
val tanggal = LocalDate.of(2026, 1, 15)
val tanggalJson = LocalDateSerializer.serialize(tanggal)
println("LocalDate : $tanggal")
println("JSON : $tanggalJson")
val tanggalBack = LocalDateSerializer.deserialize(tanggalJson)
println("Deserialized : $tanggalBack")
println("Identik : ${tanggalBack == tanggal}")
println()
println("--- 2. BigDecimal <-> string presisi ---")
val jumlah = BigDecimal("123456789.123456789")
.setScale(6, RoundingMode.HALF_UP)
val jumlahJson = BigDecimalSerializer.serialize(jumlah)
println("BigDecimal : $jumlah (presisi ${jumlah.scale()} digit)")
println("JSON : $jumlahJson")
val jumlahBack = BigDecimalSerializer.deserialize(jumlahJson)
println("Deserialized : $jumlahBack")
println("Identik : ${jumlahBack.compareTo(jumlah) == 0}")
println()
println("--- Bahaya: number JSON vs string ---")
val jumlahDoubleJson = "123456789.123456789"
val viaDouble = jumlahDoubleJson.toDouble()
println("Original : $jumlahDoubleJson")
println("Via toDouble(): $viaDouble (presisi hilang!)")
println("Solusi : serialize sebagai string, deserialize sebagai BigDecimal")
println()
println("--- 3. Color <-> hex string ---")
val merah = Color(255, 0, 0)
val biru = Color(0, 0, 255)
val kuning = Color(255, 255, 0)
println("Color(255,0,0) -> ${ColorSerializer.serialize(merah)}")
println("Color(0,0,255) -> ${ColorSerializer.serialize(biru)}")
println("Color(255,255,0) -> ${ColorSerializer.serialize(kuning)}")
val hex = "\"#FF7F00\""
val dariHex = ColorSerializer.deserialize(hex)
println("Hex $hex -> $dariHex (R=${dariHex.r}, G=${dariHex.g}, B=${dariHex.b})")
println()
println("--- 4. Model lengkap dengan custom serializer ---")
val trx = Transaksi(
id = 9001L,
tanggal = LocalDate.of(2026, 1, 15),
jumlah = BigDecimal("500000.00"),
warna = Color(0, 128, 0)
)
val ser = TransaksiSerializer()
val json = ser.serialize(trx)
println("Transaksi : $trx")
println("JSON : $json")
val restored = ser.deserialize(json)
println("Deserialized : $restored")
println("Identik : ${restored == trx}")
println()
println("--- Ringkasan: kapan butuh custom serializer ---")
println("- Tipe pihak ketiga yang tidak punya @Serializable (LocalDate, UUID)")
println("- Tipe yang butuh format kustom (Color -> hex, Range -> [start,end])")
println("- Presisi numerik (BigDecimal -> string)")
println("- Format JSON berbeda dari default (camelCase <-> snake_case pada nilai)")
println("- Polimorfisme manual yang lebih fleksibel dari sealed class")
}Output yang Dihasilkan:
=== Custom Serializers ===
--- 1. LocalDate <-> ISO-8601 string ---
LocalDate : 2026-01-15
JSON : "2026-01-15"
Deserialized : 2026-01-15
Identik : true
--- 2. BigDecimal <-> string presisi ---
BigDecimal : 123456789.123457 (presisi 6 digit)
JSON : "123456789.123457"
Deserialized : 123456789.123457
Identik : true
--- Bahaya: number JSON vs string ---
Original : 123456789.123456789
Via toDouble(): 1.2345678912345679E8 (presisi hilang!)
Solusi : serialize sebagai string, deserialize sebagai BigDecimal
--- 3. Color <-> hex string ---
Color(255,0,0) -> "#FF0000"
Color(0,0,255) -> "#0000FF"
Color(255,255,0) -> "#FFFF00"
Hex "#FF7F00" -> Color(r=255, g=127, b=0) (R=255, G=127, B=0)
--- 4. Model lengkap dengan custom serializer ---
Transaksi : Transaksi(id=9001, tanggal=2026-01-15, jumlah=500000.00, warna=Color(r=0, g=128, b=0))
JSON : {"id":9001,"tanggal":"2026-01-15","jumlah":"500000.00","warna":"#008000"}
Deserialized : Transaksi(id=9001, tanggal=2026-01-15, jumlah=500000.00, warna=Color(r=0, g=128, b=0))
Identik : true
--- Ringkasan: kapan butuh custom serializer ---
- Tipe pihak ketiga yang tidak punya @Serializable (LocalDate, UUID)
- Tipe yang butuh format kustom (Color -> hex, Range -> [start,end])
- Presisi numerik (BigDecimal -> string)
- Format JSON berbeda dari default (camelCase <-> snake_case pada nilai)
- Polimorfisme manual yang lebih fleksibel dari sealed classMari kita bedah bagian kunci dari kode di atas:
interface KSerializer<T>: kontrak serializer dengandescriptor,serialize(obj),deserialize(text). Pada library nyata,descriptoradalah objekSerialDescriptoryang menjelaskan struktur tipe ke compiler.LocalDateSerializer: mengkonversiLocalDateke string ISO-8601"2026-01-15"dan sebaliknya.LocalDate.toString()sudah menghasilkan ISO-8601;LocalDate.parse()sudah dapat membaca ISO-8601. Serializer ini hanya membungkus pemanggilan tersebut.BigDecimalSerializer: mengkonversiBigDecimalke string. Penting: angka desimal besar di JSON (mis.123456789.123456789) dapat kehilangan presisi jika diparse sebagaiDouble. Dengan men-serialize sebagai string dan men-deserialize sebagaiBigDecimal, presisi terjaga.ColorSerializer: mengkonversiColor(r, g, b)ke hex string"#FF0000". Custom format seperti ini tidak dapat dihasilkan otomatis oleh plugin—kita harus menulis serializer sendiri. MethodfromHexmemparse hex string kembali keColor.TransaksiSerializer: serializer untuk modelTransaksiyang berisiLocalDate,BigDecimal, danColor. Serializer ini memanggil ketiga custom serializer di atas—pola komposisi yang lazim di proyek nyata.- Output menunjukkan round-trip identik: setiap custom serializer menghasilkan
serialize->deserialize-> object identik dengan aslinya. Tidak ada presisi atau data yang hilang. - Peringatan presisi BigDecimal: demo menunjukkan bahaya memparse JSON angka desimal sebagai
Double—"123456789.123456789".toDouble()menghasilkan1.2345678912345679E8yang berbeda dari aslinya. Solusi: serialize sebagai string.
Mendaftarkan Custom Serializer #
Custom serializer didaftarkan via anotasi @Serializable(with = ...) pada property atau class. Plugin compiler akan memakai serializer yang ditunjuk alih-alih menggenerate default.
import kotlinx.serialization.Serializable
@Serializable
data class Transaksi(
val id: Long,
@Serializable(with = LocalDateSerializer::class)
val tanggal: LocalDate,
@Serializable(with = BigDecimalSerializer::class)
val jumlah: BigDecimal,
@Serializable(with = ColorSerializer::class)
val warna: Color
)Pendaftaran juga dapat dilakukan global via SerializersModule—berguna saat serializer dipakai di banyak model.
import kotlinx.serialization.modules.SerializersModule
val module = SerializersModule {
contextual(LocalDate::class, LocalDateSerializer)
contextual(BigDecimal::class, BigDecimalSerializer)
}
val json = Json { serializersModule = module }Kapan Custom Serializer vs Alternatif #
Custom serializer bukan satu-satunya pilihan. Untuk beberapa kasus, ada alternatif yang lebih sederhana. Pertama, tipe primitif yang harus ditulis sebagai string dapat memakai @SerialName pada String field dengan transformasi manual di getter/setter. Kedua, tipe dengan representasi sederhana dapat dibungkus sebagai value class dengan @Serializable. Ketiga, tipe yang membutuhkan transformasi kompleks benar-benar membutuhkan custom serializer.
| Kondisi | Solusi |
|---|---|
| Nama field berbeda di JSON | @SerialName("name") |
| Field eksklusi | @Transient |
Tipe pihak ketiga (LocalDate, UUID) | Custom KSerializer<T> |
Format non-trivial (Color ke hex) | Custom KSerializer<T> |
| Polimorfisme dengan many subclasses | Sealed class + @Serializable |
Presisi numerik tinggi (BigDecimal) | Custom KSerializer<T> |
Referensi Lanjutan #
- Serialization
— panduan resmi untuk
KSerializerdan@Serializabledengan custom serializer
Penutup #
Custom serializer memberikan kontrol penuh atas bagaimana tipe kompleks di-serialize. LocalDateSerializer untuk tanggal, BigDecimalSerializer untuk presisi numerik, ColorSerializer untuk format kustom. Dengan pola komposisi, satu model dapat memakai beberapa custom serializer sekaligus. Chapter berikutnya akan masuk ke polymorphic serialization—bagaimana sealed class dengan beberapa subclass di-serialize ke JSON dengan discriminator field.