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:

rust
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:

text
--- 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  : null

Mari kita bedah baris-baris pentingnya:

  1. Baris 11 (let data: Value = serde_json::from_str(teks_json).unwrap();): anotasi tipe Value menggantikan peran struct — parser tetap memakai from_str yang sama, tetapi hasilnya berupa pohon enum, bukan struct Produk. JSON apa pun yang valid secara sintaks akan berhasil di-parse.
  2. Baris 14 (println!("{}", serde_json::to_string_pretty(&data).unwrap());): Value mengimplementasikan Serialize, sehingga to_string_pretty tetap bisa dipakai untuk mencetak isi pohon kembali menjadi JSON rapi — alat serialize dari Chapter sebelumnya bekerja tanpa perubahan.
  3. Baris 17 (data["nama"].as_str().unwrap_or("-")): indexing dengan kunci string mengambil Value di dalam object. Metode as_str() mencoba melihat variant Value::String dan mengembalikan Option<&str> — unwrap_or("-") menyediakan cadangan bila kunci absen atau tipenya memang bukan string.
  4. Baris 18 (data["tags"][0].as_str().unwrap_or("-")): indexing bisa dirantai — ["tags"] menembus object, [0] menembus array, lalu as_str() mengambil isinya. Pola yang sama berlaku untuk kedalaman berapa pun.
  5. 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 mengembalikan Option karena Value yang dipegang bisa saja bertipe lain.
  6. Baris 21 (let harga = data["harga"].as_i64().unwrap_or(0);): nilai as_i64() disimpan ke variabel bertipe i64 sungguhan — dari titik ini, harga adalah bilangan yang diperiksa kompilator dan siap dihitung, seperti perkalian harga * 3 pada baris 23.
  7. Baris 25 (println!("warna : {}", data["warna"]);): kunci "warna" tidak ada di JSON, dan indexing kunci absen menghasilkan Value::Null — tercetak sebagai null, 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:

rust
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:

text
{"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:

  1. Baris 5 (let mut data: Value = ...): kata kunci mut wajib karena isi pohon akan diubah — aturan ownership dan mutability dari awal Course tetap berlaku penuh pada Value.
  2. Baris 7 (data["stok"] = json!(18);): assignment lewat indexing mengganti Value pada kunci "stok" dengan Value::Number baru dari macro json!. Macro tersebut membaca literal yang mirip JSON — angka, string, boolean, bahkan object dan array utuh — dan membangun Value yang sesuai.
  3. 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.
  4. Baris 10 (let teks_balik = serde_json::to_string(&data).unwrap();): to_string men-serialize pohon menjadi JSON ringkas — arah balik dari from_str di baris 5. Urutan kunci pada keluaran kembali terurut alfabetis (harga, kategori, nama, stok) mengikuti map internal Value.

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 dan Option di setiap akses.
  • Kombinasi keduanya juga lazim: struct menampung field yang pasti ada, sementara satu field #[serde(flatten)] bertipe Value atau HashMap<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.