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:
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):
@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.
@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.
// 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:
// 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:
=== 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.