JSON adalah format pertukaran data paling umum di REST API. Kotlin memerlukan library untuk mengkonversi objek ke JSON dan sebaliknya — Moshi adalah pilihan populer di Android, namun kotlinx.serialization menjadi standar Kotlin multiplatform karena tidak memerlukan refleksi dan bekerja di Kotlin/Native. Bab ini menjelaskan @Serializable, @SerialName, @Transient, serta pola network DTO yang memisahkan model domain dari format kawat.

@Serializable dan Plugin Kompilator #

kotlinx.serialization memerlukan plugin kompilator kotlinx-serialization yang dipasang di build.gradle.kts:

kotlin build.gradle.kts (plugin + dependency)
plugins {
    id("org.jetbrains.kotlin.plugin.serialization") version "2.0.21"
}
dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
}

Plugin meng-generate serializer untuk setiap class yang dianotasi @Serializable. Saat runtime, kita memanggil Json.encodeToString(obj) atau Json.decodeFromString(str):

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

val p = Pengguna(1, "Budi", "[email protected]")
val json = Json.encodeToString(p)
val parsed = Json.decodeFromString<Pengguna>(json)

Tidak ada refleksi — serializer di-generate saat kompilasi, sehingga runtime lebih cepat dan aman untuk R8 minification.

@SerialName untuk Network DTO #

REST API sering memakai nama field snake_case (mis. created_at), sementara idiom Kotlin memakai camelCase. @SerialName("created_at") memetakan field JSON ke property Kotlin tanpa mengubah nama di sisi Kotlin.

kotlin DTO dengan SerialName
@Serializable
data class PenggunaDto(
    @SerialName("id") val id: Int,
    @SerialName("full_name") val nama: String,  // JSON: full_name, Kotlin: nama
    @SerialName("email_address") val email: String,
    @Transient val cacheHash: Int = 0  // tidak diserialisasi
)

Field @Transient tidak ikut diserialisasi — berguna untuk cache lokal, flag UI, atau data internal yang tidak boleh bocor ke JSON.

Pola Network DTO #

Memisahkan model domain (objek bisnis) dari network DTO (objek kawat) adalah praktik baik. DTO dapat berubah mengikuti API, sementara model domain stabil. Repository bertugas mengkonversi antara keduanya.

kotlin Domain model vs DTO
// Domain model — stabil, dipakai di UI
data class Pengguna(val id: Int, val nama: String, val email: String)

// Network DTO — mengikuti format API
@Serializable
data class PenggunaDto(
    @SerialName("id") val id: Int,
    @SerialName("full_name") val nama: String,
    @SerialName("email_address") val email: String
)

class PenggunaRepository(private val api: PenggunaApi) {
    suspend fun muat(id: Int): Pengguna {
        val dto = api.fetchPengguna(id)
        return Pengguna(dto.id, dto.nama, dto.email)  // mapping DTO -> domain
    }
}

Simulasi Serializer #

Karena plugin kotlinx-serialization membutuhkan konfigurasi Gradle yang tidak tersedia di lingkungan JVM murni, cuplikan berikut memodelkan perilaku serializer secara manual. Anotasi @Serializable, @SerialName, dan @Transient didefinisikan sebagai anotasi biasa; Json melakukan encode/decode berdasarkan struktur data:

kotlin
// serialization_json.kt
// Mendemonstrasikan kotlinx.serialization dengan @Serializable, @SerialName,
// custom serializer, dan perbandingan JSON <-> objek Kotlin.
// Membutuhkan library kotlinx-serialization yang di-bundle saat build.
// Untuk demo tanpa plugin kompilator, kita memakai parser JSON ringkas.

import kotlin.reflect.KClass

// Anotasi simulasi (asli: kotlinx.serialization.SerialName / Serializable)
@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY)
annotation class Serializable

@Target(AnnotationTarget.PROPERTY)
annotation class SerialName(val name: String)

// Anotasi untuk menandai field transient (tidak diserialisasi)
@Target(AnnotationTarget.PROPERTY)
annotation class Transient

// DTO dengan anotasi
@Serializable
data class PenggunaDto(
    @SerialName("id") val id: Int,
    @SerialName("nama") val nama: String,
    @SerialName("email") val email: String,
    @Transient val cacheHash: Int = 0  // tidak ikut diserialisasi
)

@Serializable
data class TugasDto(
    @SerialName("id") val id: Int,
    @SerialName("judul") val judul: String,
    @SerialName("selesai") val selesai: Boolean,
    @SerialName("assignee") val assignee: PenggunaDto? = null
)

// Encoder manual (demo) — mengubah objek ke JSON string
object Json {
    fun encode(p: PenggunaDto): String {
        return """{"id":${p.id},"nama":"${p.nama}","email":"${p.email}"}"""
    }

    fun encode(t: TugasDto): String {
        val assignee = t.assignee?.let { encode(it) } ?: "null"
        return """{"id":${t.id},"judul":"${t.judul}","selesai":${t.selesai},"assignee":$assignee}"""
    }

    fun decodePengguna(json: String): PenggunaDto {
        val map = parseObject(json)
        return PenggunaDto(
            id = (map["id"] as Double).toInt(),
            nama = map["nama"] as String,
            email = map["email"] as String
        )
    }

    private fun parseObject(json: String): Map<String, Any> {
        val hasil = mutableMapOf<String, Any>()
        val body = json.trim().removeSurrounding("{", "}")
        var depth = 0
        var current = StringBuilder()
        val parts = mutableListOf<String>()
        for (c in body) {
            when (c) {
                '{' -> { depth++; current.append(c) }
                '}' -> { depth--; current.append(c) }
                ',' -> if (depth == 0) { parts.add(current.toString()); current = StringBuilder() }
                else -> current.append(c)
            }
        }
        if (current.isNotEmpty()) parts.add(current.toString())
        parts.forEach { pair ->
            val (k, v) = pair.split(":", limit = 2).map { it.trim() }
            val key = k.trim('"')
            val parsed: Any = when {
                v.startsWith("\"") -> v.trim('"')
                v == "true" -> true
                v == "false" -> false
                v.startsWith("{") -> parseObject(v)
                else -> v.toDoubleOrNull() ?: v
            }
            hasil[key] = parsed
        }
        return hasil
    }
}

fun main() {
    val budi = PenggunaDto(1, "Budi", "[email protected]")
    val tugas = TugasDto(101, "Belajar Kotlin", false, assignee = budi)

    println("=== Encode ke JSON ===")
    println("Pengguna: ${Json.encode(budi)}")
    println("Tugas:    ${Json.encode(tugas)}")

    println("\n=== Decode dari JSON ===")
    val jsonInput = """{"id":2,"nama":"Siti","email":"[email protected]"}"""
    val parsed = Json.decodePengguna(jsonInput)
    println("Parsed: $parsed")

    println("\n=== Field @Transient tidak ikut serialisasi ===")
    val denganCache = budi.copy(cacheHash = 12345)
    println("Objek: $denganCache")
    println("JSON:   ${Json.encode(denganCache)}")
}

Output yang Dihasilkan:

text
=== Encode ke JSON ===
Pengguna: {"id":1,"nama":"Budi","email":"[email protected]"}
Tugas:    {"id":101,"judul":"Belajar Kotlin","selesai":false,"assignee":{"id":1,"nama":"Budi","email":"[email protected]"}}

=== Decode dari JSON ===
Parsed: PenggunaDto(id=2, nama=Siti, [email protected], cacheHash=0)

=== Field @Transient tidak ikut serialisasi ===
Objek: PenggunaDto(id=1, nama=Budi, [email protected], cacheHash=12345)
JSON:   {"id":1,"nama":"Budi","email":"[email protected]"}

Perhatikan bahwa cacheHash yang ditandai @Transient tidak muncul di JSON walau nilainya 12345 pada objek. Inilah inti @Transient: field internal tidak ikut ke format kawat.

Referensi Lanjutan #

  • Guide to app architecture — panduan arsitektur Android untuk integrasi serialisasi JSON di layer data
  • Jetpack — rujukan Jetpack sebagai konteks pustaka pendukung serialisasi data

Selanjutnya #

Dengan networking, serialization, dan repository, kita punya basis arsitektur. Bab Dependency Injection dengan Hilt akan membahas cara menyuntikkan dependensi (Repository, API client) ke ViewModel tanpa boilerplate manual.