JSON

JSON #

JSON (JavaScript Object Notation) adalah format pertukaran data paling umum di dunia web modern — digunakan untuk REST API, konfigurasi, penyimpanan data, dan komunikasi antar service. Di Kotlin ada tiga library populer: kotlinx.serialization (resmi dari JetBrains, Kotlin-first, compile-time safe), Gson (dari Google, matur dan sederhana, tapi reflection-based), dan Moshi (dari Square, lebih modern dari Gson, mendukung Kotlin dengan baik). Artikel ini membahas ketiganya secara mendalam dengan fokus utama pada kotlinx.serialization sebagai pilihan idiomatis untuk proyek Kotlin modern.

Memilih Library JSON #

flowchart TD
    A{Proyek baru\nKotlin-first?} -- Ya --> B["kotlinx.serialization\nResmi JetBrains, compile-time safe\nMultiplatform, no reflection"]
    A -- Tidak --> C{Sudah ada\nkode Java?}
    C -- Ya --> D["Gson\nMatur, zero config, interop mudah"]
    C -- Tidak --> E{Butuh\nperforma tinggi?}
    E -- Ya --> F["Moshi dengan\ncode gen (kapt/ksp)"]
    E -- Tidak --> B
Aspekkotlinx.serializationGsonMoshi
PendekatanCompile-time (plugin)Reflection runtimeReflection + codegen
Kotlin-first✓ SepenuhnyaPartial✓ Baik
Null safety✓ EnforcedPartial✓ Baik
Default value✓ Native✗ Tidak✓ Dengan adapter
Multiplatform✓ KMP ready✗ JVM only✗ JVM only
PerformaSangat baikSedangBaik
KonfigurasiMinimalMinimalPerlu adapter

kotlinx.serialization — Pilihan Utama #

Setup #

// build.gradle.kts
plugins {
    kotlin("jvm") version "2.0.0"
    kotlin("plugin.serialization") version "2.0.0"  // plugin wajib
}

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
}

Encode dan Decode Dasar #

import kotlinx.serialization.*
import kotlinx.serialization.json.*

@Serializable
data class Pengguna(
    val id: Int,
    val nama: String,
    val email: String
)

fun main() {
    val pengguna = Pengguna(1, "Budi Santoso", "[email protected]")

    // Encode: objek → JSON string
    val json = Json.encodeToString(pengguna)
    println(json)
    // {"id":1,"nama":"Budi Santoso","email":"[email protected]"}

    // Decode: JSON string → objek
    val kembali = Json.decodeFromString<Pengguna>(json)
    println(kembali.nama)  // Budi Santoso

    // Encode list
    val daftar = listOf(
        Pengguna(1, "Budi", "[email protected]"),
        Pengguna(2, "Sari", "[email protected]")
    )
    val jsonDaftar = Json.encodeToString(daftar)
    println(jsonDaftar)
    // [{"id":1,"nama":"Budi","email":"[email protected]"},{"id":2,...}]

    // Decode list
    val kembaliDaftar = Json.decodeFromString<List<Pengguna>>(jsonDaftar)
    println(kembaliDaftar.size)  // 2
}

Konfigurasi Json #

Buat instance Json kustom untuk mengontrol perilaku serialisasi:

// Instance kustom — buat sekali, pakai berkali-kali
val jsonPretty = Json {
    prettyPrint = true           // format output dengan indentasi
    ignoreUnknownKeys = true     // abaikan field JSON yang tidak ada di kelas
    isLenient = true             // izinkan JSON tidak standar (koma trailing, dll)
    encodeDefaults = false       // jangan encode field dengan nilai default
    explicitNulls = false        // jangan encode field null
    coerceInputValues = true     // konversi tipe jika bisa (mis: string → enum)
}

@Serializable
data class Produk(
    val id: Int,
    val nama: String,
    val harga: Double,
    val kategori: String = "Umum",  // nilai default
    val deskripsi: String? = null   // nullable dengan default null
)

val produk = Produk(1, "Laptop", 15_000_000.0)

// Dengan encodeDefaults = false: tidak encode 'kategori' dan 'deskripsi'
val json1 = Json { encodeDefaults = false }.encodeToString(produk)
println(json1)
// {"id":1,"nama":"Laptop","harga":1.5E7}

// Dengan encodeDefaults = true: encode semua field termasuk default
val json2 = Json { encodeDefaults = true }.encodeToString(produk)
println(json2)
// {"id":1,"nama":"Laptop","harga":1.5E7,"kategori":"Umum","deskripsi":null}

// pretty print
println(jsonPretty.encodeToString(produk))
// {
//     "id": 1,
//     "nama": "Laptop",
//     "harga": 1.5E7
// }

Anotasi Serialisasi #

@SerialName — Ubah Nama Field di JSON #

@Serializable
data class ResponsAPI(
    @SerialName("user_id")     val userId: Int,
    @SerialName("full_name")   val namaLengkap: String,
    @SerialName("created_at")  val dibuatPada: String,
    @SerialName("is_active")   val isAktif: Boolean
)

// JSON: {"user_id":1,"full_name":"Budi","created_at":"2024-08-17","is_active":true}
// Kotlin: ResponsAPI(userId=1, namaLengkap="Budi", dibuatPada="2024-08-17", isAktif=true)

val json = """{"user_id":42,"full_name":"Sari Dewi","created_at":"2024-01-15","is_active":true}"""
val respons = Json.decodeFromString<ResponsAPI>(json)
println(respons.namaLengkap)  // Sari Dewi

@Transient — Jangan Sertakan dalam JSON #

@Serializable
data class Pengguna(
    val id: Int,
    val nama: String,
    val email: String,
    @Transient val passwordHash: String = "",  // tidak di-serialize, wajib punya default
    @Transient val tokenSesi: String = ""       // tidak masuk JSON sama sekali
)

val pengguna = Pengguna(1, "Budi", "[email protected]", "hash123", "token456")
val json = Json.encodeToString(pengguna)
println(json)
// {"id":1,"nama":"Budi","email":"[email protected]"}
// passwordHash dan tokenSesi tidak ada di output!

Nullable dan Default Value #

@Serializable
data class Profil(
    val id: Int,
    val nama: String,
    val bio: String? = null,           // nullable, default null
    val website: String? = null,
    val followers: Int = 0,            // default value
    val verified: Boolean = false
)

// JSON minimal — field dengan default tidak wajib ada
val jsonMinimal = """{"id":1,"nama":"Budi"}"""
val profil = Json { ignoreUnknownKeys = true }.decodeFromString<Profil>(jsonMinimal)
println(profil)
// Profil(id=1, nama=Budi, bio=null, website=null, followers=0, verified=false)

// JSON lengkap
val jsonLengkap = """
    {
        "id": 2,
        "nama": "Sari",
        "bio": "Developer & Writer",
        "followers": 1500,
        "verified": true
    }
""".trimIndent()
val profilLengkap = Json { ignoreUnknownKeys = true }.decodeFromString<Profil>(jsonLengkap)
println(profilLengkap.followers)  // 1500

Struktur JSON Bersarang #

@Serializable
data class Alamat(
    val jalan: String,
    val kota: String,
    val provinsi: String,
    @SerialName("kode_pos") val kodePos: String
)

@Serializable
data class Pesanan(
    val id: String,
    val pengguna: Pengguna,
    val alamatKirim: Alamat,
    val produk: List<ItemPesanan>,
    val total: Double
)

@Serializable
data class ItemPesanan(
    val produkId: Int,
    val nama: String,
    val harga: Double,
    val jumlah: Int
) {
    val subtotal: Double
        @Transient get() = harga * jumlah  // computed property — tidak di-serialize
}

val pesanan = Pesanan(
    id = "ORD-001",
    pengguna = Pengguna(1, "Budi", "[email protected]"),
    alamatKirim = Alamat("Jl. Merdeka No. 1", "Jakarta", "DKI Jakarta", "10110"),
    produk = listOf(
        ItemPesanan(1, "Laptop", 15_000_000.0, 1),
        ItemPesanan(2, "Mouse", 250_000.0, 2)
    ),
    total = 15_500_000.0
)

println(Json { prettyPrint = true }.encodeToString(pesanan))

Enum Serialization #

@Serializable
enum class StatusPesanan {
    @SerialName("pending")    MENUNGGU,
    @SerialName("processing") DIPROSES,
    @SerialName("shipped")    DIKIRIM,
    @SerialName("delivered")  SELESAI,
    @SerialName("cancelled")  DIBATALKAN
}

@Serializable
data class Pesanan2(
    val id: String,
    val status: StatusPesanan
)

val pesanan = Pesanan2("ORD-001", StatusPesanan.DIKIRIM)
println(Json.encodeToString(pesanan))
// {"id":"ORD-001","status":"shipped"}  ← menggunakan SerialName

val dari = Json.decodeFromString<Pesanan2>("""{"id":"ORD-002","status":"delivered"}""")
println(dari.status)  // SELESAI

Polymorphism dan Sealed Class #

kotlinx.serialization mendukung polymorphism untuk sealed class:

@Serializable
sealed class HasilOperasi {
    @Serializable
    @SerialName("success")
    data class Berhasil(val data: String, val kode: Int = 200) : HasilOperasi()

    @Serializable
    @SerialName("error")
    data class Gagal(val pesan: String, val kode: Int) : HasilOperasi()

    @Serializable
    @SerialName("loading")
    object Memuat : HasilOperasi()
}

val json = Json { classDiscriminator = "tipe" }  // nama field discriminator

val berhasil: HasilOperasi = HasilOperasi.Berhasil("Data berhasil diambil")
val gagal: HasilOperasi = HasilOperasi.Gagal("Server error", 500)
val memuat: HasilOperasi = HasilOperasi.Memuat

println(json.encodeToString(berhasil))
// {"tipe":"success","data":"Data berhasil diambil","kode":200}

println(json.encodeToString(gagal))
// {"tipe":"error","pesan":"Server error","kode":500}

// Decode ke sealed class — discriminator menentukan subtype
val decoded = json.decodeFromString<HasilOperasi>("""{"tipe":"error","pesan":"Not found","kode":404}""")
when (decoded) {
    is HasilOperasi.Berhasil -> println("Sukses: ${decoded.data}")
    is HasilOperasi.Gagal    -> println("Error ${decoded.kode}: ${decoded.pesan}")
    is HasilOperasi.Memuat   -> println("Loading...")
}

Custom Serializer #

Untuk tipe yang tidak didukung secara langsung, buat serializer kustom:

import java.time.LocalDate
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*

// Serializer untuk LocalDate (tidak @Serializable secara native)
object LocalDateSerializer : KSerializer<LocalDate> {
    override val descriptor = PrimitiveSerialDescriptor("LocalDate", PrimitiveKind.STRING)

    override fun serialize(encoder: Encoder, value: LocalDate) {
        encoder.encodeString(value.toString())  // "2024-08-17"
    }

    override fun deserialize(decoder: Decoder): LocalDate {
        return LocalDate.parse(decoder.decodeString())
    }
}

@Serializable
data class Acara(
    val nama: String,
    @Serializable(with = LocalDateSerializer::class)
    val tanggal: LocalDate,
    val lokasi: String
)

val acara = Acara("Konferensi Kotlin", LocalDate.of(2024, 8, 17), "Jakarta")
val json = Json.encodeToString(acara)
println(json)
// {"nama":"Konferensi Kotlin","tanggal":"2024-08-17","lokasi":"Jakarta"}

val kembali = Json.decodeFromString<Acara>(json)
println(kembali.tanggal.year)  // 2024

Parsing JSON Dinamis dengan JsonElement #

Untuk JSON dengan struktur yang tidak diketahui atau berubah-ubah:

val jsonString = """
    {
        "versi": "2.0",
        "data": {
            "pengguna": [
                {"id": 1, "nama": "Budi"},
                {"id": 2, "nama": "Sari"}
            ],
            "total": 2
        },
        "metadata": {
            "waktu": "2024-08-17T10:30:00Z",
            "sumber": "API"
        }
    }
""".trimIndent()

val element = Json.parseToJsonElement(jsonString)
val jsonObj = element.jsonObject

// Akses nilai dengan aman
val versi = jsonObj["versi"]?.jsonPrimitive?.content
println(versi)  // 2.0

val total = jsonObj["data"]?.jsonObject?.get("total")?.jsonPrimitive?.int
println(total)  // 2

val pengguna = jsonObj["data"]?.jsonObject?.get("pengguna")?.jsonArray
pengguna?.forEach { item ->
    val nama = item.jsonObject["nama"]?.jsonPrimitive?.content
    println(nama)
}
// Budi
// Sari

// Build JsonElement secara programatik
val builtJson = buildJsonObject {
    put("nama", "Produk Baru")
    put("harga", 99_000)
    putJsonArray("tag") {
        add("elektronik")
        add("sale")
    }
    putJsonObject("dimensi") {
        put("lebar", 30)
        put("tinggi", 20)
    }
}
println(builtJson.toString())

Gson — Interop dengan Java #

Gson adalah pilihan tepat ketika kamu perlu interoperabilitas mudah dengan kode Java yang sudah ada, atau ketika proyek sudah menggunakan Gson sebelumnya.

// build.gradle.kts
// implementation("com.google.code.gson:gson:2.10.1")

import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken

data class Pengguna(val id: Int, val nama: String, val email: String)

fun main() {
    val gson = GsonBuilder()
        .setPrettyPrinting()
        .serializeNulls()
        .create()

    // Encode
    val pengguna = Pengguna(1, "Budi", "[email protected]")
    val json = gson.toJson(pengguna)
    println(json)

    // Decode
    val kembali = gson.fromJson(json, Pengguna::class.java)
    println(kembali.nama)

    // Decode list — butuh TypeToken untuk generics
    val jsonList = """[{"id":1,"nama":"A","email":"[email protected]"},{"id":2,"nama":"B","email":"[email protected]"}]"""
    val tipe = object : TypeToken<List<Pengguna>>() {}.type
    val daftar: List<Pengguna> = gson.fromJson(jsonList, tipe)
    println(daftar.size)  // 2
}

Keterbatasan Gson di Kotlin #

// MASALAH 1: Gson mengabaikan default value — field null meski ada default
data class Config(val host: String = "localhost", val port: Int = 5432)

val gson = Gson()
val config = gson.fromJson("{}", Config::class.java)
println(config.host)  // null ← bug! seharusnya "localhost"

// MASALAH 2: Gson tidak memahami nullable Kotlin
data class NullableTest(val nilai: String?)
val test = gson.fromJson("""{"nilai":null}""", NullableTest::class.java)
// Ini OK, tapi Gson tidak bisa enforce non-null di level compile

// SOLUSI: gunakan kotlinx.serialization untuk proyek Kotlin baru

Moshi — Alternatif Modern #

Moshi dari Square menawarkan type-safety yang lebih baik dari Gson:

// build.gradle.kts
// implementation("com.squareup.moshi:moshi:1.15.0")
// implementation("com.squareup.moshi:moshi-kotlin:1.15.0")

import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import com.squareup.moshi.Types

val moshi = Moshi.Builder()
    .addLast(KotlinJsonAdapterFactory())
    .build()

data class Produk(val id: Int, val nama: String, val harga: Double)

// Encode
val adapter = moshi.adapter(Produk::class.java)
val produk = Produk(1, "Laptop", 15_000_000.0)
val json = adapter.toJson(produk)
println(json)  // {"id":1,"nama":"Laptop","harga":1.5E7}

// Decode
val kembali = adapter.fromJson(json)
println(kembali?.nama)  // Laptop

// List
val listType = Types.newParameterizedType(List::class.java, Produk::class.java)
val listAdapter = moshi.adapter<List<Produk>>(listType)
val daftar = listAdapter.fromJson("""[{"id":1,"nama":"A","harga":100}]""")
println(daftar?.size)  // 1

// Moshi mengembalikan null jika parsing gagal (tidak throw exception seperti Gson)
val invalid = adapter.fromJson("invalid json")
println(invalid)  // null

Ringkasan #

  • kotlinx.serialization untuk proyek Kotlin baru — compile-time safety, no reflection overhead, multiplatform ready, dan null safety terintegrasi. Ini adalah standar yang direkomendasikan JetBrains untuk semua proyek Kotlin.
  • @SerialName untuk mapping nama field — ketika nama field JSON berbeda dari nama properti Kotlin (misalnya API menggunakan snake_case tapi Kotlin menggunakan camelCase), gunakan @SerialName("nama_json").
  • @Transient untuk field rahasia — field yang tidak boleh masuk JSON (password, token, data internal) harus ditandai @Transient. Wajib punya nilai default.
  • ignoreUnknownKeys = true di production — API yang kamu konsumsi mungkin menambahkan field baru. Tanpa ini, setiap field baru di respons API akan crash aplikasimu.
  • encodeDefaults = false untuk payload lebih kecil — jangan sertakan field dengan nilai default dalam output JSON. Berguna untuk mengurangi ukuran payload.
  • Sealed class untuk polymorphism — gunakan sealed class dengan @SerialName untuk tipe respons yang bisa berupa salah satu dari beberapa subtipe. Discriminator field menentukan subtype saat decode.
  • Custom serializer untuk tipe eksternal — untuk tipe yang tidak bisa ditandai @Serializable (seperti LocalDate, UUID, kelas dari library pihak ketiga), implementasikan KSerializer<T>.
  • Gson untuk interop Java — jika proyek sudah menggunakan Gson atau butuh integrasi dengan kode Java yang ada, Gson tetap valid. Tapi waspadai keterbatasannya dengan default value dan nullable Kotlin.

← Sebelumnya: Mocking   Berikutnya: YAML →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact