Lima Chapter sebelumnya berdiri di atas satu asumsi: bentuk JSON sudah kita ketahui saat menulis kode, sehingga struct Produk bisa dirancang lebih dulu dan derive memetakan field ke kunci. Asumsi tersebut runtuh pada sejumlah situasi nyata — respons API yang mengembalikan field berbeda tergantung status, berkas konfigurasi yang kuncinya bebas ditambah pengguna, JSON yang cukup diteruskan ke tujuan lain tanpa perlu dipahami isinya, atau skrip kilat yang hanya ingin mencuplik dua nilai dari sebuah payload. Untuk semua kasus tersebut, serde_json menyediakan tipe Value: representasi JSON yang lengkap namun bertipe dinamis, tanpa struct dan tanpa derive. Chapter penutup section ini membedah bentuk enum Value, mem-parse JSON bebas ke dalamnya, mengakses nilai lewat indexing, mengubah isinya, lalu men-serialize kembali menjadi teks JSON.
Value: Pohon Enum untuk JSON #
Tipe serde_json::Value adalah enum dengan enam variant yang persis mengikuti enam bentuk data JSON:
| Variant | Bentuk JSON | Contoh |
| — | — |
| Value::Null | null | null |
| Value::Bool | boolean | true |
| Value::Number | number | 15000, 4.5 |
| Value::String | string | "Kopi Torabika" |
| Value::Array | array | ["padat", "kemasan-250g"] |
| Value::Object | object | {"nama": "Kopi Torabika"} |
Kekuatan Value terletak pada sifatnya yang rekursif: Value::Array membungkus Vec<Value> sehingga isinya bisa berupa Value jenis apa pun termasuk array lain, dan Value::Object membungkus map dari kunci string ke Value. Struktur bersarang seberapa dalam pun JSON-nya, representasinya tetap sebatas kombinasi enam variant tersebut — pohon yang utuh, tanpa satu pun struct kustom. Kompilator tidak tahu apa pun tentang makna isi pohon tersebut; pemeriksaan bentuk data berpindah dari waktu kompilasi ke waktu program berjalan.
Parsing Bebas dan Akses Nilai #
Program berikut mem-parse teks JSON apa adanya — tanpa struct pendamping — lalu mencetak seluruh isinya dan mengambil nilai dari beberapa bagian:
use serde_json::Value;
fn main() {
let teks_json = r#"{
"nama": "Kopi Torabika",
"harga": 15000,
"tags": ["padat", "kemasan-250g"],
"stok_tersedia": true
}"#;
let data: Value = serde_json::from_str(teks_json).unwrap();
println!("--- isi lengkap ---");
println!("{}", serde_json::to_string_pretty(&data).unwrap());
println!("--- akses per bagian ---");
println!("nama : {}", data["nama"].as_str().unwrap_or("-"));
println!("tags[0]: {}", data["tags"][0].as_str().unwrap_or("-"));
println!("stok : {}", data["stok_tersedia"].as_bool().unwrap_or(false));
let harga = data["harga"].as_i64().unwrap_or(0);
println!("harga : {}", harga);
println!("total 3 pcs: {}", harga * 3);
println!("warna : {}", data["warna"]);
}Output yang Dihasilkan:
--- isi lengkap ---
{
"harga": 15000,
"nama": "Kopi Torabika",
"stok_tersedia": true,
"tags": [
"padat",
"kemasan-250g"
]
}
--- akses per bagian ---
nama : Kopi Torabika
tags[0]: padat
stok : true
harga : 15000
total 3 pcs: 45000
warna : nullMari kita bedah baris-baris pentingnya:
- Baris 11 (
let data: Value = serde_json::from_str(teks_json).unwrap();): anotasi tipeValuemenggantikan peran struct — parser tetap memakaifrom_stryang sama, tetapi hasilnya berupa pohon enum, bukan structProduk. JSON apa pun yang valid secara sintaks akan berhasil di-parse. - Baris 14 (
println!("{}", serde_json::to_string_pretty(&data).unwrap());):ValuemengimplementasikanSerialize, sehinggato_string_prettytetap bisa dipakai untuk mencetak isi pohon kembali menjadi JSON rapi — alat serialize dari Chapter sebelumnya bekerja tanpa perubahan. - Baris 17 (
data["nama"].as_str().unwrap_or("-")): indexing dengan kunci string mengambilValuedi dalam object. Metodeas_str()mencoba melihat variantValue::Stringdan mengembalikanOption<&str>—unwrap_or("-")menyediakan cadangan bila kunci absen atau tipenya memang bukan string. - Baris 18 (
data["tags"][0].as_str().unwrap_or("-")): indexing bisa dirantai —["tags"]menembus object,[0]menembus array, laluas_str()mengambil isinya. Pola yang sama berlaku untuk kedalaman berapa pun. - Baris 19 (
data["stok_tersedia"].as_bool().unwrap_or(false)): setiap variant punya metode konversi masing-masing —as_bool()untuk boolean,as_i64()untuk bilangan,as_f64()untuk pecahan. Semuanya mengembalikanOptionkarenaValueyang dipegang bisa saja bertipe lain. - Baris 21 (
let harga = data["harga"].as_i64().unwrap_or(0);): nilaias_i64()disimpan ke variabel bertipei64sungguhan — dari titik ini,hargaadalah bilangan yang diperiksa kompilator dan siap dihitung, seperti perkalianharga * 3pada baris 23. - Baris 25 (
println!("warna : {}", data["warna"]);): kunci"warna"tidak ada di JSON, dan indexing kunci absen menghasilkanValue::Null— tercetak sebagainull, tanpa panic dan tanpa galat.
Dua kejutan kecil pada keluaran perlu dijelaskan. Pertama, urutan kunci pada blok isi lengkap berubah menjadi harga, nama, stok_tersedia, tags — berbeda dari urutan penulisan di teks masukan — karena map internal Value mengurutkan kunci secara alfabetis. Kedua, kunci absen tidak memicu galat missing field seperti pada deserialize struct; Value tidak punya daftar field wajib, sehingga ketidakhadiran kunci hanya tampak sebagai null.
Mengubah Value dan Marshal Kembali #
Value bukan sekadar pembaca: isinya bisa diubah, kunci baru bisa ditambahkan, dan hasil akhirnya bisa di-serialize kembali menjadi teks JSON. Macro json! dari serde_json memudahkan pembuatan nilai baru di sisi kanan assignment:
use serde_json::{json, Value};
fn main() {
let teks_json = r#"{"nama": "Kopi Torabika", "harga": 15000, "stok": 24}"#;
let mut data: Value = serde_json::from_str(teks_json).unwrap();
data["stok"] = json!(18);
data["kategori"] = json!("Minuman");
let teks_balik = serde_json::to_string(&data).unwrap();
println!("{}", teks_balik);
}Output yang Dihasilkan:
{"harga":15000,"kategori":"Minuman","nama":"Kopi Torabika","stok":18}Stok berubah dari 24 menjadi 18, kunci "kategori" baru tercipta, dan seluruh pohon kembali menjadi teks JSON ringkas. Mari kita bedah baris-baris pentingnya:
- Baris 5 (
let mut data: Value = ...): kata kuncimutwajib karena isi pohon akan diubah — aturan ownership dan mutability dari awal Course tetap berlaku penuh padaValue. - Baris 7 (
data["stok"] = json!(18);): assignment lewat indexing menggantiValuepada kunci"stok"denganValue::Numberbaru dari macrojson!. Macro tersebut membaca literal yang mirip JSON — angka, string, boolean, bahkan object dan array utuh — dan membangunValueyang sesuai. - Baris 8 (
data["kategori"] = json!("Minuman");): meng-assign pada kunci yang belum ada justru membuat kunci tersebut — berbeda dari aturan map yang ketat, indexing assignment bersifat pemaaf terhadap kunci baru. - Baris 10 (
let teks_balik = serde_json::to_string(&data).unwrap();):to_stringmen-serialize pohon menjadi JSON ringkas — arah balik darifrom_strdi baris 5. Urutan kunci pada keluaran kembali terurut alfabetis (harga,kategori,nama,stok) mengikuti map internalValue.
Kapan Value, Kapan Struct #
Value dan struct ber derive tidak bersaing — keduanya menjawab situasi yang berbeda. Pedoman praktisnya ringkas:
- Bentuk JSON sudah diketahui dan stabil: pakai struct dengan derive. Kesalahan kunci dan tipe tertangkap saat kompilasi, dan akses field secepat akses struct biasa.
- Bentuk JSON baru diketahui saat program berjalan, kuncinya bebas, atau isinya hanya diteruskan tanpa diolah: pakai
Value. Fleksibilitas dibayar dengan pemeriksaan runtime danOptiondi setiap akses. - Kombinasi keduanya juga lazim: struct menampung field yang pasti ada, sementara satu field
#[serde(flatten)]bertipeValueatauHashMap<String, Value>menampung sisa kunci yang tidak terdefinisi.
Ringkasan #
serde_json::Value merepresentasikan JSON bebas sebagai pohon enum enam variant — Null, Bool, Number, String, Array, Object — yang bersarang secara rekursif tanpa struct pendamping. Fungsi from_str mem-parse JSON apa pun menjadi Value, indexing data["kunci"] dan data["array"][0] menembus pohon, metode as_str, as_i64, dan as_bool mengonversi ke tipe Rust lewat Option, dan kunci absen menghasilkan null tanpa galat. Isi pohon bisa dimutasi lewat assignment dengan bantuan macro json!, lalu di-serialize kembali memakai to_string yang sama dari Chapter-Chapter sebelumnya. Dengan seluruh perjalanan section ini — dari derive, parsing, galat asli, attribute rename/skip/default/flatten, hingga Value — kita kini membawa data Rust keluar masuk dunia JSON dengan kontrol penuh: skema ketat lewat struct saat bentuknya diketahui, dan tipe dinamis lewat Value saat bentuknya liar.