Model data dunia nyata sering berbentuk hierarchy—Pesan dengan subclass TextPesan, ImagePesan, VideoPesan. Saat di-serialize ke JSON, kita perlu cara untuk merekam tipe mana yang sedang disimpan agar dapat di-deserialize dengan benar. kotlinx.serialization menyelesaikan ini dengan discriminator field—biasanya "type"—yang menyimpan nama subclass. Saat deserialize, library membaca discriminator dan memilih subclass yang sesuai. Pada chapter ini kita akan memahami pola sealed class serialization, melihat discriminator field di JSON output, dan bagaimana @JsonClassDiscriminator mengontrol nama discriminator.
Sealed Class dan Discriminator #
Sealed class adalah kandidat ideal untuk polymorphic serialization karena kompilator mengetahui semua subclass—tidak ada yang dapat ditambahkan dari file lain. Saat kita menandai sealed class dengan @Serializable, library otomatis mendaftar semua subclass yang ditandai @Serializable.
import kotlinx.serialization.*
@Serializable
@JsonClassDiscriminator("message_type")
sealed class Pesan {
abstract val id: Long
abstract val pengirim: String
}
@Serializable
@SerialName("text")
data class TextPesan(
override val id: Long,
override val pengirim: String,
val text: String
) : Pesan()@JsonClassDiscriminator("message_type") mengganti nama discriminator dari default "type" menjadi "message_type". @SerialName("text") mengganti nama subclass di JSON dari default (TextPesan) menjadi text. Kontrol eksplisit ini penting agar JSON stabil saat nama class Kotlin berubah.
Simulasi Polymorphic Serializer #
Karena kotlinx.serialization memerlukan compiler plugin, demo berikut mensimulasikan pola sealed class serialization dengan when yang menentukan tipe dan menulis discriminator field.
// polymorphic_serialization.kt
// Polymorphic serialization: sealed class dengan discriminator field.
// Demo: hierarchy Pesan (TextPesan, ImagePesan, VideoPesan) + @JsonClassDiscriminator.
// --- Wrapper anotasi (simulasi) ---
@Target(AnnotationTarget.CLASS)
annotation class KSerializable
@Target(AnnotationTarget.PROPERTY)
annotation class SerialName(val name: String)
@Target(AnnotationTarget.CLASS)
annotation class JsonClassDiscriminator(val name: String)
// --- Sealed class hierarchy ---
@JsonClassDiscriminator("type")
@KSerializable
sealed class Pesan {
abstract val id: Long
abstract val pengirim: String
}
data class TextPesan(
override val id: Long,
override val pengirim: String,
val text: String
) : Pesan()
data class ImagePesan(
override val id: Long,
override val pengirim: String,
@property:SerialName("image_url") val imageUrl: String,
@property:SerialName("image_width") val width: Int,
@property:SerialName("image_height") val height: Int
) : Pesan()
data class VideoPesan(
override val id: Long,
override val pengirim: String,
@property:SerialName("video_url") val videoUrl: String,
val durasiDetik: Int
) : Pesan()
// --- Polymorphic serializer (simulasi) ---
class PesanSerializer {
fun serialize(p: Pesan): String {
val type = when (p) {
is TextPesan -> "text"
is ImagePesan -> "image"
is VideoPesan -> "video"
}
val parts = mutableListOf<String>()
// Discriminator field pertama
parts.add("\"type\":\"$type\"")
// Field bersama
parts.add("\"id\":${p.id}")
parts.add("\"pengirim\":\"${esc(p.pengirim)}\"")
// Field spesifik per subtype
when (p) {
is TextPesan -> parts.add("\"text\":\"${esc(p.text)}\"")
is ImagePesan -> {
parts.add("\"image_url\":\"${esc(p.imageUrl)}\"")
parts.add("\"image_width\":${p.width}")
parts.add("\"image_height\":${p.height}")
}
is VideoPesan -> {
parts.add("\"video_url\":\"${esc(p.videoUrl)}\"")
parts.add("\"durasi_detik\":${p.durasiDetik}")
}
}
return "{${parts.joinToString(",")}}"
}
fun deserialize(text: String): Pesan {
val type = extractString(text, "type") ?: error("type wajib")
val id = extractLong(text, "id") ?: error("id wajib")
val pengirim = extractString(text, "pengirim") ?: error("pengirim wajib")
return when (type) {
"text" -> {
val isi = extractString(text, "text") ?: ""
TextPesan(id, pengirim, isi)
}
"image" -> {
val url = extractString(text, "image_url") ?: ""
val w = extractInt(text, "image_width") ?: 0
val h = extractInt(text, "image_height") ?: 0
ImagePesan(id, pengirim, url, w, h)
}
"video" -> {
val url = extractString(text, "video_url") ?: ""
val dur = extractInt(text, "durasi_detik") ?: 0
VideoPesan(id, pengirim, url, dur)
}
else -> throw IllegalArgumentException("Tipe tidak dikenali: $type")
}
}
}
object PolymorphicJson {
private val ser = PesanSerializer()
fun encode(p: Pesan): String = ser.serialize(p)
fun decode(text: String): Pesan = ser.deserialize(text)
fun encodeList(items: List<Pesan>): String {
return "[${items.joinToString(",") { encode(it) }}]"
}
fun decodeList(text: String): List<Pesan> {
val inner = text.removePrefix("[").removeSuffix("]")
// Cari tiap object di dalam array
val items = mutableListOf<Pesan>()
var depth = 0
var start = -1
for (i in inner.indices) {
val c = inner[i]
when (c) {
'{' -> { if (depth == 0) start = i; depth++ }
'}' -> { depth--; if (depth == 0 && start >= 0) {
items.add(decode(inner.substring(start, i + 1)))
start = -1
} }
}
}
return items
}
}
// --- Helpers ---
private fun esc(s: String): String = s.replace("\\", "\\\\").replace("\"", "\\\"")
private fun extractLong(json: String, key: String): Long? =
Regex("\"$key\"\\s*:\\s*(-?\\d+)").find(json)?.groupValues?.get(1)?.toLong()
private fun extractInt(json: String, key: String): Int? =
Regex("\"$key\"\\s*:\\s*(-?\\d+)").find(json)?.groupValues?.get(1)?.toInt()
private fun extractString(json: String, key: String): String? =
Regex("\"$key\"\\s*:\\s*\"((?:[^\"\\\\]|\\\\.)*)\"").find(json)?.groupValues?.get(1)?.replace("\\\"", "\"")?.replace("\\\\", "\\")
// --- Demo ---
fun main() {
println("=== Polymorphic Serialization ===\n")
val pesanList = listOf<Pesan>(
TextPesan(id = 1, pengirim = "Budi", text = "Halo, apa kabar?"),
ImagePesan(id = 2, pengirim = "Siti",
imageUrl = "https://example.com/img/001.jpg",
width = 1920, height = 1080),
VideoPesan(id = 3, pengirim = "Dewi",
videoUrl = "https://example.com/vid/002.mp4",
durasiDetik = 145)
)
println("--- Serialize per item ---")
for (p in pesanList) {
val json = PolymorphicJson.encode(p)
println("Kotlin : ${p.javaClass.simpleName}")
println("JSON : $json")
println()
}
println("--- Serialize list ---")
val jsonArray = PolymorphicJson.encodeList(pesanList)
println(jsonArray)
println()
println("--- Deserialize per item ---")
for (p in pesanList) {
val json = PolymorphicJson.encode(p)
val restored = PolymorphicJson.decode(json)
val restoredType = restored.javaClass.simpleName
println("Type : $restoredType")
println("Restored: $restored")
println("Identik : ${restored == p}")
println()
}
println("--- Decode list ---")
val restoredList = PolymorphicJson.decodeList(jsonArray)
println("Jumlah pesan: ${restoredList.size}")
restoredList.forEachIndexed { i, p ->
println(" [$i] ${p.javaClass.simpleName}: id=${p.id}, dari=${p.pengirim}")
}
println()
println("--- Discriminator yang dipakai ---")
println("@JsonClassDiscriminator(\"type\")")
println("Output JSON selalu menyertakan field 'type' pertama:")
println(" - \"type\":\"text\" untuk TextPesan")
println(" - \"type\":\"image\" untuk ImagePesan")
println(" - \"type\":\"video\" untuk VideoPesan")
println()
println("--- Tipe tidak dikenali ---")
try {
val jsonTidakDikenal = """{"type":"audio","id":99,"pengirim":"Hacker"}"""
PolymorphicJson.decode(jsonTidakDikenal)
} catch (e: IllegalArgumentException) {
println("Ditolak: ${e.message}")
}
println()
println("--- Polimorfisme pada interface open class ---")
println("Selain sealed class, kotlinx.serialization juga mendukung:")
println("- Sealed class: otomatis mendaftar semua subclass")
println("- Open class + @Serializable: perlu @SerializersModule")
println("- Polymorphic<Any>: bawaan, dengan discriminator 'type'")
}Output yang Dihasilkan:
=== Polymorphic Serialization ===
--- Serialize per item ---
Kotlin : TextPesan
JSON : {"type":"text","id":1,"pengirim":"Budi","text":"Halo, apa kabar?"}
Kotlin : ImagePesan
JSON : {"type":"image","id":2,"pengirim":"Siti","image_url":"https://example.com/img/001.jpg","image_width":1920,"image_height":1080}
Kotlin : VideoPesan
JSON : {"type":"video","id":3,"pengirim":"Dewi","video_url":"https://example.com/vid/002.mp4","durasi_detik":145}
--- Serialize list ---
[{"type":"text","id":1,"pengirim":"Budi","text":"Halo, apa kabar?"},{"type":"image","id":2,"pengirim":"Siti","image_url":"https://example.com/img/001.jpg","image_width":1920,"image_height":1080},{"type":"video","id":3,"pengirim":"Dewi","video_url":"https://example.com/vid/002.mp4","durasi_detik":145}]
--- Deserialize per item ---
Type : TextPesan
Restored: TextPesan(id=1, pengirim=Budi, text=Halo, apa kabar?)
Identik : true
Type : ImagePesan
Restored: ImagePesan(id=2, pengirim=Siti, imageUrl=https://example.com/img/001.jpg, width=1920, height=1080)
Identik : true
Type : VideoPesan
Restored: VideoPesan(id=3, pengirim=Dewi, videoUrl=https://example.com/vid/002.mp4, durasiDetik=145)
Identik : true
--- Decode list ---
Jumlah pesan: 3
[0] TextPesan: id=1, dari=Budi
[1] ImagePesan: id=2, dari=Siti
[2] VideoPesan: id=3, dari=Dewi
--- Discriminator yang dipakai ---
@JsonClassDiscriminator("type")
Output JSON selalu menyertakan field 'type' pertama:
- "type":"text" untuk TextPesan
- "type":"image" untuk ImagePesan
- "type":"video" untuk VideoPesan
--- Tipe tidak dikenali ---
Ditolak: Tipe tidak dikenali: audio
--- Polimorfisme pada interface open class ---
Selain sealed class, kotlinx.serialization juga mendukung:
- Sealed class: otomatis mendaftar semua subclass
- Open class + @Serializable: perlu @SerializersModule
- Polymorphic<Any>: bawaan, dengan discriminator 'type'Mari kita bedah bagian kunci dari kode di atas:
@JsonClassDiscriminator("type"): marker anotasi pada sealed class yang mengatur nama discriminator. Di library nyata, ini adalahkotlinx.serialization.json.JsonClassDiscriminator.sealed class Pesan: root hierarchy denganabstract val iddanabstract val pengirim. Tiga subclass:TextPesan,ImagePesan,VideoPesan. Sealed menjamin tidak ada subclass lain—library dapat mendaftar semuanya.PesanSerializer.serialize(p): menulis discriminator"type"pertama, lalu field bersama (id,pengirim), lalu field spesifik per subtype.whenmenentukan tipe dan menulis field yang sesuai—textuntukTextPesan,image_urldan dimensi untukImagePesan, dll.PesanSerializer.deserialize(text): membaca discriminator"type"pertama, lalu memilih subclass yang sesuai viawhen. Setiap cabang membaca field spesifik subtype. Tipe tidak dikenali menyebabkanIllegalArgumentException.PolymorphicJson.encodeList/decodeList: menangani list polimorfik—JSON array berisi object dengan tipe berbeda. Decoder mem-parse setiap object dengan state machine brace-tracking untuk menemukan batas object.- Output per item: tiga JSON object dengan discriminator berbeda—
"type":"text","type":"image","type":"video". Field bersama (id,pengirim) muncul di semua; field spesifik hanya muncul di subtype terkait. - Round-trip identik: setiap pesan di-serialize lalu di-deserialize menghasilkan object identik dengan aslinya—tipe subclass, field, dan value semuanya terjaga.
Default Discriminator dan @SerialName #
Tanpa @JsonClassDiscriminator, default discriminator adalah "type". Tanpa @SerialName pada subclass, default nama adalah simple class name (mis. TextPesan). Konvensi yang baik: selalu pakai @SerialName pada subclass agar JSON stabil—nama class Kotlin dapat berubah selama refactor tanpa memengaruhi kontrak JSON.
// Default: discriminator="type", subclass name="TextPesan"
{"type":"TextPesan","id":1,...}
// Dengan @SerialName("text"): discriminator="type", subclass name="text"
{"type":"text","id":1,...}
// Dengan @JsonClassDiscriminator("kind") + @SerialName("text"):
{"kind":"text","id":1,...}Polimorfisme pada Open Class #
Selain sealed class, kotlinx.serialization juga mendukung polimorfisme pada open class dan interface. Namun, subclass tidak otomatis terdaftar—kita harus mendaftarnya manual via SerializersModule. Pola ini berguna saat hierarchy tersebar di banyak modul yang tidak dapat disatukan sebagai sealed class.
import kotlinx.serialization.modules.SerializersModule
val module = SerializersModule {
polymorphic(Pesan::class) {
subclass(TextPesan::class)
subclass(ImagePesan::class)
subclass(VideoPesan::class)
}
}
val json = Json { serializersModule = module }Referensi Lanjutan #
- Serialization
— panduan resmi
kotlinx.serializationuntuk class seal dan polimorfisme
Penutup #
Polymorphic serialization dengan sealed class menggabungkan type safety Kotlin dengan fleksibilitas JSON. Discriminator field menyimpan tipe subclass; @SerialName mengontrol nama; @JsonClassDiscriminator mengontrol nama discriminator. Round-trip identik menjamin tidak ada data yang hilang. Chapter berikutnya akan masuk ke JSON Schema validation—bagaimana memvalidasi JSON masukan terhadap skema yang didefinisikan sebelum deserialisasi.