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.

kotlin sealed class + discriminator (library nyata)
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.

kotlin
// 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:

text
=== 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:

  1. @JsonClassDiscriminator("type"): marker anotasi pada sealed class yang mengatur nama discriminator. Di library nyata, ini adalah kotlinx.serialization.json.JsonClassDiscriminator.
  2. sealed class Pesan: root hierarchy dengan abstract val id dan abstract val pengirim. Tiga subclass: TextPesan, ImagePesan, VideoPesan. Sealed menjamin tidak ada subclass lain—library dapat mendaftar semuanya.
  3. PesanSerializer.serialize(p): menulis discriminator "type" pertama, lalu field bersama (id, pengirim), lalu field spesifik per subtype. when menentukan tipe dan menulis field yang sesuai—text untuk TextPesan, image_url dan dimensi untuk ImagePesan, dll.
  4. PesanSerializer.deserialize(text): membaca discriminator "type" pertama, lalu memilih subclass yang sesuai via when. Setiap cabang membaca field spesifik subtype. Tipe tidak dikenali menyebabkan IllegalArgumentException.
  5. 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.
  6. 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.
  7. 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.

kotlin Default vs eksplisit
// 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.

kotlin Polimorfisme open class (library nyata)
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.serialization untuk 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.