JSON adalah format pertukaran data paling populer untuk REST API. Library kotlinx.serialization menyediakan JSON serialization dengan anotasi yang mengontrol pemetaan field Kotlin ke JSON. @SerialName mengubah nama field di JSON—berguna saat backend memakai snake_case sementara Kotlin memakai camelCase. @Transient mengecualikan field sensitif seperti password atau memo internal. @Optional membuat field opsional dengan default value. Pada chapter ini kita akan melihat penggunaan ketiga anotasi tersebut, konfigurasi Json seperti prettyPrint dan ignoreUnknownKeys, serta nested object serialization.

Anotasi @SerialName, @Transient, @Optional #

@SerialName mengganti nama field saat serialization. Field Kotlin id dapat dipetakan ke product_id di JSON. @Transient mengecualikan field dari JSON sama sekali—berguna untuk passwordHash, internalMemo, atau field komputasi. @Optional membuat field opsional—diisi default value jika tidak ada di JSON.

kotlin Contoh: model dengan anotasi
import kotlinx.serialization.*

@Serializable
data class Produk(
    @SerialName("product_id") val id: Long,
    @SerialName("product_name") val nama: String,
    @SerialName("unit_price") val harga: Long,
    @SerialName("in_stock") val stok: Int,
    @Optional val deskripsi: String? = null,
    @Transient val internalMemo: String = ""
)

Saat produk1 di-serialize, field id ditulis sebagai product_id di JSON. Field internalMemo sama sekali tidak muncul. Field deskripsi hanya muncul jika tidak null.

Simulasi JSON Serialization #

Karena kotlinx.serialization memerlukan compiler plugin, demo berikut mensimulasikan perilaku plugin dengan marker anotasi buatan dan serializer manual. Pola yang ditampilkan—pemetaan @SerialName, eksklusi @Transient, opsional @Optional, dan pretty print—adalah pola yang dihasilkan plugin pada proyek nyata.

kotlin
// json_serialization.kt
// JSON serialization dengan kotlinx.serialization (simulasi).
// Mencakup: @SerialName, @Transient, @Optional, Json configuration.

// --- Marker anotasi (simulasi 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

@Target(AnnotationTarget.PROPERTY)
annotation class Default(val value: String)

// --- Model ---
@KSerializable
data class Produk(
    @property:SerialName("product_id") val id: Long,
    @property:SerialName("product_name") val nama: String,
    @property:SerialName("unit_price") val harga: Long,
    @property:SerialName("in_stock") val stok: Int,
    @property:Optional val deskripsi: String? = null,
    @property:Transient val internalMemo: String = ""
)

@KSerializable
data class Pesanan(
    @property:SerialName("order_id") val id: Long,
    @property:SerialName("customer_name") val pelanggan: String,
    val items: List<Produk>,
    @property:SerialName("created_at") val dibuatPada: String
)

// --- Serializer (simulasi) ---
class ProdukSerializer {
    fun serialize(p: Produk): String {
        val parts = mutableListOf<String>()
        parts.add("\"product_id\":${p.id}")
        parts.add("\"product_name\":\"${esc(p.nama)}\"")
        parts.add("\"unit_price\":${p.harga}")
        parts.add("\"in_stock\":${p.stok}")
        if (p.deskripsi != null) parts.add("\"deskripsi\":\"${esc(p.deskripsi)}\"")
        // @Transient: internalMemo dilewati
        return "{${parts.joinToString(",")}}"
    }

    fun deserialize(text: String): Produk {
        val id = extractLong(text, "product_id") ?: error("product_id wajib")
        val nama = extractString(text, "product_name") ?: error("product_name wajib")
        val harga = extractLong(text, "unit_price") ?: error("unit_price wajib")
        val stok = extractInt(text, "in_stock") ?: error("in_stock wajib")
        val deskripsi = extractString(text, "deskripsi")
        return Produk(id, nama, harga, stok, deskripsi)
    }
}

class PesananSerializer {
    private val produkSer = ProdukSerializer()

    fun serialize(p: Pesanan): String {
        val parts = mutableListOf<String>()
        parts.add("\"order_id\":${p.id}")
        parts.add("\"customer_name\":\"${esc(p.pelanggan)}\"")
        parts.add("\"items\":[${p.items.joinToString(",") { produkSer.serialize(it) }}]")
        parts.add("\"created_at\":\"${esc(p.dibuatPada)}\"")
        return "{${parts.joinToString(",")}}"
    }

    fun deserialize(text: String): Pesanan {
        val id = extractLong(text, "order_id") ?: error("order_id wajib")
        val pelanggan = extractString(text, "customer_name") ?: error("customer_name wajib")
        val dibuatPada = extractString(text, "created_at") ?: error("created_at wajib")
        // Parse items array - simulasi sederhana
        val itemsText = extractArray(text, "items")
        val items = mutableListOf<Produk>()
        // Cari tiap object di dalam array
        var depth = 0
        var start = -1
        for (i in itemsText.indices) {
            val c = itemsText[i]
            when (c) {
                '{' -> { if (depth == 0) start = i; depth++ }
                '}' -> { depth--; if (depth == 0 && start >= 0) {
                    items.add(produkSer.deserialize(itemsText.substring(start, i + 1)))
                    start = -1
                } }
            }
        }
        return Pesanan(id, pelanggan, items, dibuatPada)
    }
}

// --- Json object dengan konfigurasi ---
class JsonConfig(
    val prettyPrint: Boolean = false,
    val encodeDefaults: Boolean = true,
    val ignoreUnknownKeys: Boolean = false,
    val coerceInputValues: Boolean = false
) {
    fun render(json: String): String {
        if (!prettyPrint) return json
        return prettyPrint(json)
    }

    private fun prettyPrint(json: String): String {
        val sb = StringBuilder()
        var depth = 0
        var inString = false
        for (c in json) {
            when {
                inString -> { sb.append(c); if (c == '"') inString = false }
                c == '"' -> { inString = true; sb.append(c) }
                c == '{' || c == '[' -> {
                    sb.append(c).append('\n')
                    depth++
                    sb.append("  ".repeat(depth))
                }
                c == '}' || c == ']' -> {
                    sb.append('\n')
                    depth--
                    sb.append("  ".repeat(depth))
                    sb.append(c)
                }
                c == ',' -> {
                    sb.append(c).append('\n')
                    sb.append("  ".repeat(depth))
                }
                else -> sb.append(c)
            }
        }
        return sb.toString()
    }
}

object Json {
    private val produkSer = ProdukSerializer()
    private val pesananSer = PesananSerializer()

    fun encodeProduk(p: Produk, config: JsonConfig = JsonConfig()): String {
        val json = produkSer.serialize(p)
        return config.render(json)
    }

    fun decodeProduk(text: String): Produk = produkSer.deserialize(text)

    fun encodePesanan(p: Pesanan, config: JsonConfig = JsonConfig()): String {
        val json = pesananSer.serialize(p)
        return config.render(json)
    }

    fun decodePesanan(text: String): Pesanan = pesananSer.deserialize(text)
}

// --- 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("\\\\", "\\")
private fun extractArray(json: String, key: String): String {
    val match = Regex("\"$key\"\\s*:\\s*\\[").find(json) ?: return "[]"
    var depth = 1
    var i = match.range.last + 1
    while (i < json.length && depth > 0) {
        when (json[i]) {
            '[' -> depth++
            ']' -> depth--
        }
        i++
    }
    return json.substring(match.range.last + 1, i - 1)
}

// --- Demo ---

fun main() {
    println("=== JSON Serialization ===\n")

    val produk1 = Produk(
        id = 101L,
        nama = "Sepatu Lari X",
        harga = 350_000L,
        stok = 15,
        deskripsi = "Sepatu ringan untuk lari jarak jauh",
        internalMemo = "supplier-A-margin-20%"
    )

    val produk2 = Produk(
        id = 102L,
        nama = "Tas Ransel 30L",
        harga = 280_000L,
        stok = 8,
        deskripsi = null,  // tidak ada deskripsi
        internalMemo = "supplier-B-margin-15%"
    )

    println("--- Produk 1 (dengan deskripsi) ---")
    val json1 = Json.encodeProduk(produk1)
    println("Object : $produk1")
    println("JSON   : $json1")
    println("Catatan: internalMemo tidak ada di JSON (@Transient)")
    println()

    println("--- Produk 2 (tanpa deskripsi) ---")
    val json2 = Json.encodeProduk(produk2)
    println("Object : $produk2")
    println("JSON   : $json2")
    println("Catatan: deskripsi tidak ditulis karena null (@Optional)")
    println()

    println("--- Pretty print ---")
    val pretty = Json.encodeProduk(produk1, JsonConfig(prettyPrint = true))
    println(pretty)
    println()

    println("--- Pesanan dengan nested items ---")
    val pesanan = Pesanan(
        id = 5001L,
        pelanggan = "Budi Santoso",
        items = listOf(produk1, produk2),
        dibuatPada = "2026-01-15T14:30:00Z"
    )
    val pesananJson = Json.encodePesanan(pesanan, JsonConfig(prettyPrint = true))
    println(pesananJson)
    println()

    println("--- Round-trip decode ---")
    val restored = Json.decodePesanan(pesananJson)
    println("Restored        : $restored")
    println("Identik (logic) : true jika field @Transient diabaikan")
    println("Catatan         : internalMemo kosong karena @Transient (tidak di-serialize)")
    println()

    println("--- Decode JSON dengan key tidak dikenali ---")
    val jsonDenganExtra = """{"product_id":103,"product_name":"Topi","unit_price":50000,"in_stock":20,"warna":"merah"}"""
    val restored3 = Json.decodeProduk(jsonDenganExtra)
    println("JSON input : $jsonDenganExtra")
    println("Restored   : $restored3")
    println("Catatan: key 'warna' diabaikan (ignoreUnknownKeys default false)")
    println()

    println("--- Konfigurasi Json ---")
    println("prettyPrint       : true  -> output multi-baris dengan indentasi")
    println("encodeDefaults    : true  -> tulis field walau bernilai default")
    println("ignoreUnknownKeys : true  -> lewati key tidak dikenali (berguna untuk API berubah)")
    println("coerceInputValues : true  -> null pada field non-null diisi default")
}

Output yang Dihasilkan:

text
=== JSON Serialization ===

--- Produk 1 (dengan deskripsi) ---
Object : Produk(id=101, nama=Sepatu Lari X, harga=350000, stok=15, deskripsi=Sepatu ringan untuk lari jarak jauh, internalMemo=supplier-A-margin-20%)
JSON   : {"product_id":101,"product_name":"Sepatu Lari X","unit_price":350000,"in_stock":15,"deskripsi":"Sepatu ringan untuk lari jarak jauh"}
Catatan: internalMemo tidak ada di JSON (@Transient)

--- Produk 2 (tanpa deskripsi) ---
Object : Produk(id=102, nama=Tas Ransel 30L, harga=280000, stok=8, deskripsi=null, internalMemo=supplier-B-margin-15%)
JSON   : {"product_id":102,"product_name":"Tas Ransel 30L","unit_price":280000,"in_stock":8}
Catatan: deskripsi tidak ditulis karena null (@Optional)

--- Pretty print ---
{
  "product_id":101,
  "product_name":"Sepatu Lari X",
  "unit_price":350000,
  "in_stock":15,
  "deskripsi":"Sepatu ringan untuk lari jarak jauh"
}

--- Pesanan dengan nested items ---
{
  "order_id":5001,
  "customer_name":"Budi Santoso",
  "items":[
    {
      "product_id":101,
      "product_name":"Sepatu Lari X",
      "unit_price":350000,
      "in_stock":15,
      "deskripsi":"Sepatu ringan untuk lari jarak jauh"
    },
    {
      "product_id":102,
      "product_name":"Tas Ransel 30L",
      "unit_price":280000,
      "in_stock":8
    }
  ],
  "created_at":"2026-01-15T14:30:00Z"
}

--- Round-trip decode ---
Restored        : Pesanan(id=5001, pelanggan=Budi Santoso, items=[Produk(id=101, nama=Sepatu Lari X, harga=350000, stok=15, deskripsi=Sepatu ringan untuk lari jarak jauh, internalMemo=), Produk(id=102, nama=Tas Ransel 30L, harga=280000, stok=8, deskripsi=null, internalMemo=)], dibuatPada=2026-01-15T14:30:00Z)
Identik (logic) : true jika field @Transient diabaikan
Catatan         : internalMemo kosong karena @Transient (tidak di-serialize)

--- Decode JSON dengan key tidak dikenali ---
JSON input : {"product_id":103,"product_name":"Topi","unit_price":50000,"in_stock":20,"warna":"merah"}
Restored   : Produk(id=103, nama=Topi, harga=50000, stok=20, deskripsi=null, internalMemo=)
Catatan: key 'warna' diabaikan (ignoreUnknownKeys default false)

--- Konfigurasi Json ---
prettyPrint       : true  -> output multi-baris dengan indentasi
encodeDefaults    : true  -> tulis field walau bernilai default
ignoreUnknownKeys : true  -> lewati key tidak dikenali (berguna untuk API berubah)
coerceInputValues : true  -> null pada field non-null diisi default

Mari kita bedah bagian kunci dari kode di atas:

  1. annotation class KSerializable, SerialName, Transient, Optional: empat marker anotasi yang mensimulasikan kotlinx.serialization. Pada library nyata, anotasi ini sudah disediakan dan plugin compiler menghasilkan serializer.
  2. data class Produk: model dengan lima field. Empat field wajib (id, nama, harga, stok) memakai @SerialName untuk pemetaan snake_case. Field deskripsi nullable memakai @Optional—di-serialize hanya jika tidak null. Field internalMemo memakai @Transient—tidak pernah muncul di JSON.
  3. data class Pesanan: model dengan nested List<Produk>. Field items di-serialize sebagai JSON array berisi object produk. Field dibuatPada memakai @SerialName("created_at").
  4. ProdukSerializer: implementasi serializer. Saat serialize, membaca anotasi: @Transient field dilewati, @Optional field hanya ditulis jika tidak null. Saat deserialize, field wajib divalidasi—error jika tidak ada.
  5. PesananSerializer: serializer yang menangani nested array. Method deserialize mem-parse JSON array dengan state machine sederhana—melacak kedalaman brace {} untuk menemukan batas setiap object produk.
  6. JsonConfig: konfigurasi serialization. prettyPrint=true menghasilkan JSON multi-baris dengan indentasi 2 spasi. ignoreUnknownKeys=true melewatkan key tidak dikenali saat decode.
  7. Output pretty print: JSON multi-baris yang mudah dibaca—setiap key-value di baris baru, nested object diindentasi. Berguna untuk debugging dan log.
  8. Round-trip decode: object di-serialize ke JSON, lalu di-deserialize kembali. Field internalMemo kosong setelah decode karena @Transient—data sensitif tidak bocor.

Konfigurasi Json #

Object Json di kotlinx.serialization.json.Json menerima konfigurasi via builder. Empat opsi paling umum: prettyPrint untuk output multi-baris, encodeDefaults untuk menulis field walau default value, ignoreUnknownKeys untuk melewatkan key tidak dikenali, coerceInputValues untuk mengisi default saat nilai JSON null pada field non-null.

kotlin Konfigurasi Json di library nyata
import kotlinx.serialization.json.Json

val json = Json {
    prettyPrint = true
    encodeDefaults = true
    ignoreUnknownKeys = true
    coerceInputValues = true
}

val output = json.encodeToString(produk)

ignoreUnknownKeys=true sangat berguna saat API berubah dan menambah field baru. Kode lama yang tidak tahu field baru tetap dapat mendecode JSON baru tanpa error.

Nested Object dan Array #

JSON mendukung nested object dan array—Pesanan berisi List<Produk>. kotlinx.serialization menangani nesting secara rekursif: serializer Pesanan memanggil serializer Produk untuk setiap elemen items. Tidak perlu kode tambahan—cukup anotasi @Serializable di kedua class.

kotlin Nested object di JSON
{
  "order_id": 5001,
  "customer_name": "Budi Santoso",
  "items": [
    {"product_id": 101, "product_name": "Sepatu Lari X"},
    {"product_id": 102, "product_name": "Tas Ransel 30L"}
  ],
  "created_at": "2026-01-15T14:30:00Z"
}

Referensi Lanjutan #

  • Serialization — panduan resmi kotlinx.serialization untuk JSON termasuk Json config dan @SerialName

Penutup #

JSON serialization dengan kotlinx.serialization menggabungkan kemudahan anotasi dengan kontrol halus. @SerialName untuk pemetaan nama, @Transient untuk field sensitif, @Optional untuk field opsional. Konfigurasi Json mengatur perilaku format. Chapter berikutnya akan masuk ke custom serializer—bagaimana menulis serializer kustom untuk tipe kompleks seperti LocalDate atau BigDecimal yang belum didukung secara bawaan.