JSON (JavaScript Object Notation) adalah format pertukaran data paling populer untuk REST API, file konfigurasi, dan message queue. Meskipun Kotlin memiliki pustaka resmi kotlinx.serialization untuk produksi, memahami cara mem-parsing JSON secara manual memberi wawasan tentang bagaimana parser bekerja dan kapan menggunakan pustaka eksternal.
Struktur JSON #
JSON mendukung enam tipe data:
| Tipe | Sintaks | Contoh |
|---|---|---|
| Object | { "k": v, ... } | {"nama": "Budi"} |
| Array | [v, v, ...] | [1, 2, 3] |
| String | "..." (escape \n, \", \\) | "halo\n" |
| Number | integer atau float | 42, 3.14 |
| Boolean | true / false | true |
| Null | null | null |
JSON tidak mendukung komentar, koma trailing, atau string tunggal — kesalahan umum yang membuat parser ketat menolak file.
Parser JSON Manual di Kotlin #
Sebelum membahas pustaka eksternal, mari kita tulis parser JSON sederhana menggunakan sealed class dan recursive descent. Pendekatan ini mengajarkan struktur data JSON dan teknik parsing yang sama dipakai oleh library profesional.
import java.io.StringReader
import java.io.StringWriter
// === Parser JSON sederhana tanpa library eksternal ===
// Mendukung: object { "k": v }, array [v, v], string, number, boolean, null
sealed class JsonValue {
data class Str(val v: String) : JsonValue() {
override fun toString() = "\"$v\""
}
data class Num(val v: Double) : JsonValue() {
override fun toString() = if (v % 1.0 == 0.0) v.toInt().toString() else v.toString()
}
data class Bool(val v: Boolean) : JsonValue() {
override fun toString() = v.toString()
}
object Null : JsonValue() {
override fun toString() = "null"
}
data class Arr(val items: List<JsonValue>) : JsonValue() {
override fun toString() = items.joinToString(", ", "[", "]")
}
data class Obj(val entries: List<Pair<String, JsonValue>>) : JsonValue() {
override fun toString() = entries.joinToString(", ") { "\"${it.first}\": ${it.second}" }.let { "{$it}" }
}
}
class JsonParser(private val src: String) {
private var pos = 0
fun parse(): JsonValue {
skipWs()
val v = parseValue()
skipWs()
require(pos == src.length) { "Karakter tersisa setelah JSON valid" }
return v
}
private fun parseValue(): JsonValue {
skipWs()
return when (val c = peek()) {
'"' -> JsonValue.Str(parseString())
't', 'f' -> parseBool()
'n' -> parseNull()
'[' -> parseArray()
'{' -> parseObject()
else -> if (c == '-' || c.isDigit()) parseNumber() else throw IllegalArgumentException("Unexpected: $c at $pos")
}
}
private fun parseString(): String {
require(peek() == '"')
pos++
val sb = StringBuilder()
while (peek() != '"') {
val c = src[pos++]
if (c == '\\') {
when (val e = src[pos++]) {
'n' -> sb.append('\n')
't' -> sb.append('\t')
'"' -> sb.append('"')
'\\' -> sb.append('\\')
'/' -> sb.append('/')
else -> sb.append(e)
}
} else sb.append(c)
}
pos++
return sb.toString()
}
private fun parseNumber(): JsonValue.Num {
val start = pos
if (peek() == '-') pos++
while (pos < src.length && (src[pos].isDigit() || src[pos] == '.' || src[pos] == 'e' || src[pos] == 'E' || src[pos] == '+' || src[pos] == '-')) pos++
return JsonValue.Num(src.substring(start, pos).toDouble())
}
private fun parseBool(): JsonValue.Bool {
if (src.startsWith("true", pos)) { pos += 4; return JsonValue.Bool(true) }
if (src.startsWith("false", pos)) { pos += 5; return JsonValue.Bool(false) }
throw IllegalArgumentException("Invalid bool at $pos")
}
private fun parseNull(): JsonValue.Null {
if (src.startsWith("null", pos)) { pos += 4; return JsonValue.Null }
throw IllegalArgumentException("Invalid null at $pos")
}
private fun parseArray(): JsonValue.Arr {
require(peek() == '['); pos++
skipWs()
val items = mutableListOf<JsonValue>()
if (peek() == ']') { pos++; return JsonValue.Arr(items) }
while (true) {
items.add(parseValue())
skipWs()
when (peek()) {
',' -> { pos++; skipWs() }
']' -> { pos++; return JsonValue.Arr(items) }
else -> throw IllegalArgumentException("Expected , or ] at $pos")
}
}
}
private fun parseObject(): JsonValue.Obj {
require(peek() == '{'); pos++
skipWs()
val entries = mutableListOf<Pair<String, JsonValue>>()
if (peek() == '}') { pos++; return JsonValue.Obj(entries) }
while (true) {
skipWs()
val key = parseString()
skipWs()
require(peek() == ':'); pos++
val value = parseValue()
entries.add(key to value)
skipWs()
when (peek()) {
',' -> { pos++; skipWs() }
'}' -> { pos++; return JsonValue.Obj(entries) }
else -> throw IllegalArgumentException("Expected , or } at $pos")
}
}
}
private fun peek(): Char = src[pos]
private fun skipWs() { while (pos < src.length && src[pos].isWhitespace()) pos++ }
}
fun main() {
val jsonText = """
{
"nama": "Budi",
"umur": 25,
"menikah": false,
"anak": ["Andi", "Citra"],
"alamat": null
}
""".trimIndent()
val parser = JsonParser(jsonText)
val parsed = parser.parse()
println("Parsed JSON:")
println(parsed)
println()
// Akses field
val obj = parsed as JsonValue.Obj
for ((key, value) in obj.entries) {
println(" $key = $value")
}
// Cek tipe
val nama = obj.entries.find { it.first == "nama" }?.second
println("\nNama: $nama (tipe: ${nama?.javaClass?.simpleName})")
val anak = obj.entries.find { it.first == "anak" }?.second as JsonValue.Arr
println("Jumlah anak: ${anak.items.size}")
println("Anak pertama: ${anak.items[0]}")
}Output yang Dihasilkan:
Parsed JSON:
{"nama": "Budi", "umur": 25, "menikah": false, "anak": ["Andi", "Citra"], "alamat": null}
nama = "Budi"
umur = 25
menikah = false
anak = ["Andi", "Citra"]
alamat = null
Nama: "Budi" (tipe: Str)
Jumlah anak: 2
Anak pertama: "Andi"Mari kita bedah bagian-bagian penting parser di atas:
sealed class JsonValue: tipe data algebraic yang merepresentasikan enam tipe JSON.sealedmembuatwhenexhaustive — compiler memaksa kita menangani semua kasus.data class Str/Num/Bool/Arr/Obj: subclass dengan field terstruktur.data classotomatis menghasilkanequals,hashCode, dantoString.object Null: singleton karenanulltidak punya state.JsonParser(src: String): menyimpan string sumber dan posisi parser saat ini (pos).parseValue(): dispatch ke parser yang sesuai berdasarkan karakter pertama. Ini adalah inti recursive descent.parseString(): membaca karakter demi karakter, mendekode escape sequence seperti\n,\",\\.parseObject(): loop membaca key-value pair hingga}. Setiap key harus berupa string, value bisa tipe apa pun.parseArray(): loop membaca value hingga].skipWs(): melewati whitespace (spasi, tab, newline) — JSON mengizinkan whitespace di mana pun kecuali di dalam string.
Mengapa Memakai Library untuk Produksi #
Parser manual di atas adalah ilustrasi — produksi memerlukan fitur yang lebih lengkap:
- Unicode escape:
\u0041untuk huruf “A”. - Presisi angka: BigInteger untuk angka besar, BigDecimal untuk angka desimal.
- Streaming parsing: membaca file JSON besar tanpa memuat seluruhnya ke memori.
- Schema validation: memastikan field wajib ada dan tipe sesuai kontrak.
- Error reporting: posisi baris dan kolom, bukan hanya offset karakter.
kotlinx.serialization #
Untuk produksi, Kotlin menyediakan kotlinx.serialization:
import kotlinx.serialization.*
import kotlinx.serialization.json.*
@Serializable
data class Pengguna(val nama: String, val umur: Int)
fun main() {
val u = Pengguna("Budi", 25)
val json = Json.encodeToString(u)
println(json) // {"nama":"Budi","umur":25}
val loaded = Json.decodeFromString<Pengguna>(json)
println(loaded)
}Library ini bersifat compiler-aware: anotasi @Serializable memicu compiler menghasilkan serializer otomatis saat build. Tidak ada reflection runtime, sehingga aman untuk Kotlin/Native dan Kotlin/JS.
Best Practice #
- Selalu gunakan pustaka yang teruji (
kotlinx.serialization,Moshi,Gson) untuk produksi. - Tetapkan schema JSON eksplisit dengan
@Serializabledan field wajib. - Tangani
MissingFieldExceptiondengan baik — ini mengindikasikan data tidak sesuai kontrak. - Untuk JSON dari sumber tidak terpercaya, batasi ukuran maksimum dan timeout parser untuk mencegah serangan DoS.
Referensi Lanjutan #
- Kotlin stdlib kotlin.io
— paket
kotlin.iountuk menulis dan membaca hasil JSON - Java SE 21 java.io package
— ringkasan paket
java.ioJava SE 21 untukReaderdanWriterJSON
Selanjutnya #
Dengan JSON di tangan, kita dapat membangun pipeline file yang robust — menggabungkan baca, tulis, validasi, dan error handling. Materi ini akan dibahas pada chapter Robust File Processing.