Data di dalam program Rust hidup sebagai struct di memori: field nama berupa String, field harga berupa u64, semuanya tertata mengikuti tipe yang kita definisikan. Begitu data harus keluar dari program — disimpan ke berkas konfigurasi, dikirim ke HTTP API, atau diteruskan ke antrean pesan — bentuk struct tidak bisa berpindah apa adanya, karena media pertukaran tersebut hanya mengenal teks. JSON menjadi bahasa pengantar de facto untuk pertukaran data semacam itu, dan mengubah struct menjadi teks JSON lalu sebaliknya adalah pekerjaan yang rawan kalau dikerjakan dengan tangan kosong. Ekosistem Rust menjawab kebutuhan ini dengan serde, kerangka serialisasi yang menjadi fondasi hampir seluruh pertukaran data di ekosistem Rust. Chapter ini memetakan masalahnya, membedah pembagian tugas di dalam keluarga crate serde, lalu menyiapkan proyek pertama kita.

Data Program dan Media Pertukaran #

Struct Rust dan teks JSON hidup di dua dunia yang berbeda aturan. Struct menyusun data dengan tipe yang ketat: kompilator memastikan field harga hanya menerima angka dan field nama hanya menerima teks. JSON, sebaliknya, hanyalah teks dengan aturan format yang longgar — pasangan kunci dan nilai yang bisa berupa string, number, boolean, array, object, atau null. Kedua dunia tersebut dipertemukan lewat dua proses bernama tetap:

  • Serialization: mengubah data bertipe di memori menjadi teks yang siap dikirim atau disimpan.
  • Deserialization: membaca teks tersebut kembali menjadi data bertipe di memori, lengkap dengan pemeriksaan bahwa teksnya cocok dengan tipe yang dituju.

Kebutuhan dua arah tersebut muncul di banyak skenario nyata: aplikasi membaca berkas konfigurasi saat start, layanan web menerima dan membalas JSON lewat HTTP, pekerjaan antrean pesan bertukar payload, dan log terstruktur ditulis baris demi baris dalam bentuk JSON.

Cara Manual yang Rapuh #

Sebelum melihat solusinya, mari kita rasakan dulu masalahnya. Program kecil berikut merakit teks JSON untuk sebuah produk kopi menggunakan macro format! — pendekatan pertama yang terlintak di kepala banyak pemula:

rust
fn main() {
    let nama = "Kopi \"Arabika\" Premium";
    let harga = 15000;

    let teks_json = format!("{{\"nama\": \"{}\", \"harga\": {}}}", nama, harga);
    println!("Hasil rakitan manual: {}", teks_json);

    let hasil: Result<serde_json::Value, _> = serde_json::from_str(&teks_json);
    match hasil {
        Ok(_) => println!("JSON valid"),
        Err(galat) => println!("JSON ditolak parser: {}", galat),
    }
}

Output yang Dihasilkan:

text
Hasil rakitan manual: {"nama": "Kopi "Arabika" Premium", "harga": 15000}
JSON ditolak parser: expected `,` or `}` at line 1 column 17

Rakitan manual tersebut terlihat benar di mata manusia, tetapi parser JSON menolaknya mentah-mentah. Mari kita bedah kode di atas:

  1. Baris 2 (let nama = "Kopi \"Arabika\" Premium";): variabel nama menyimpan teks yang sah di sisi Rust — tanda \" adalah escape untuk tanda kutip di dalam string literal Rust.
  2. Baris 5 (let teks_json = format!(...)): macro format! menyisipkan isi nama mentah-mentah ke dalam cetakan JSON. Nilai "Kopi "Arabika" Premium" tidak di-escape sebagai isi string JSON, sehingga tanda kutip di tengah teks merusak batas string JSON.
  3. Baris 8 (let hasil: Result<serde_json::Value, _> = ...): program memvalidasi rakitannya sendiri dengan meng-parse teks hasil format! memakai serde_json::from_str, lalu menyimpan hasilnya sebagai Result tanpa memaksa unwrap.
  4. Baris 11 (Err(galat) => ...): cabang galat terpilih, dan pesan expected ',' or '}' menandakan parser berhenti di kolom 17 — tepat setelah string "Kopi " ditutup, parser menemukan huruf A yang bukan koma maupun penutup object.

Masalahnya bukan sekadar satu tanda kutip. Perakitan manual juga harus mengurus koma antar field, kurung kurawal yang benar, angka tanpa tanda kutip, dan parsing arah balik yang jauh lebih sulit dari penulisan. Setiap aturan yang kita kelola dengan tangan adalah satu celah galat baru.

Serde dan Pembagian Tugasnya #

Serde — singkatan dari serialization/deserialization — memecahkan masalah tersebut dengan membagi pekerjaan ke tiga komponen yang perannya tegas:

KomponenPeran
Crate serdeMendefinisikan trait Serialize dan Deserialize sebagai kontrak antara tipe data kita dan format data
Crate serde_jsonMengimplementasikan kontrak tersebut untuk format JSON: parser dan writer JSON yang cepat
Crate serde_deriveProc macro yang menuliskan implementasi trait untuk struct kita secara otomatis

Trait Serialize dan Deserialize pada crate serde berperan seperti kontrak: tipe yang mengimplementasikan Serialize berjanji “saya tahu cara memaparkan isi diri saya”, dan tipe yang mengimplementasikan Deserialize berjanji “saya tahu cara membangun diri saya dari data mentah”. Crate serde_json kemudian menjembatani kontrak tersebut ke format JSON secara konkret — membaca teks JSON menjadi struktur internal, lalu menyerahkannya ke Deserialize, atau menerima paparan dari Serialize lalu menuliskannya menjadi teks JSON. Alur dua arah tersebut dapat digambarkan sebagai berikut:

Menyiapkan Proyek #

Kita akan memakai satu proyek cargo kecil untuk seluruh eksperimen di section ini. Buat proyek baru, lalu tambahkan kedua crate:

bash
cargo init toko-kopi
cd toko-kopi
cargo add serde --features derive
cargo add serde_json

Isi Cargo.toml setelah kedua perintah tersebut berjalan:

toml
[package]
name = "toko-kopi"
version = "0.1.0"
edition = "2021"

[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"

Baris features = ["derive"] pada crate serde adalah kunci yang sering terlewat. Tanpa feature tersebut, crate serde hanya mengeksport trait Serialize dan Deserialize, sedangkan macro derive yang menulis implementasi secara otomatis tidak ikut terpasang. Feature flag dipakai di sini karena proses macro menambah waktu kompilasi — pengguna yang hanya butuh trait-nya boleh memilih tidak memuat derive.

Contoh Pertama End-to-End #

Mari kita tutup Chapter ini dengan satu putaran penuh: struct menjadi teks, lalu teks kembali menjadi struct. Tulis kode berikut ke src/main.rs:

rust
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct Tugas {
    judul: String,
    selesai: bool,
}

fn main() {
    let tugas = Tugas {
        judul: String::from("Kirim laporan stok kopi"),
        selesai: false,
    };

    let teks = serde_json::to_string(&tugas).unwrap();
    println!("Teks JSON: {}", teks);

    let hasil: Tugas = serde_json::from_str(&teks).unwrap();
    println!("Kembali ke struct: judul '{}', selesai: {}", hasil.judul, hasil.selesai);
}

Output yang Dihasilkan:

text
Teks JSON: {"judul":"Kirim laporan stok kopi","selesai":false}
Kembali ke struct: judul 'Kirim laporan stok kopi', selesai: false

Program tersebut melakukan round-trip lengkap tanpa satu pun escape manual. Mari kita bedah baris-baris pentingnya:

  1. Baris 1 (use serde::{Deserialize, Serialize};): mengimpor kedua trait sekaligus macro derive bernama sama dari crate serde.
  2. Baris 3 (#[derive(Serialize, Deserialize)]): attribute ini memerintahkan proc macro serde_derive untuk menulis implementasi Serialize dan Deserialize bagi struct Tugas pada saat kompilasi — pokok bahasan penuh Chapter berikutnya.
  3. Baris 15 (let teks = serde_json::to_string(&tugas).unwrap();): fungsi to_string menerima reference ke struct yang mengimplementasikan Serialize dan mengembalikan Result<String>; unwrap mengambil teks JSON di dalamnya karena kita tahu serialize tidak mungkin gagal pada contoh ini.
  4. Baris 16 (println!("Teks JSON: {}", teks);): teks JSON tercetak dalam bentuk ringkas — tanpa spasi setelah titik dua dan koma. Field judul menjadi kunci "judul", dan nilai false dari field selesai menjadi boolean JSON.
  5. Baris 18 (let hasil: Tugas = serde_json::from_str(&teks).unwrap();): fungsi from_str mem-parse teks JSON dan membangun struct Tugas baru — anotasi tipe : Tugas memberi tahu parser bentuk struct yang dituju.
  6. Baris 19 (println!("Kembali ke struct: ...")): field judul dan selesai terbaca kembali dengan tipe Rust aslinya — String dan bool — bukan sebagai teks mentah.

Ringkasan #

Serialisasi menjembatani struct Rust di memori dan teks JSON di dunia luar, dan perakitan manual terbukti rapuh karena escape, koma, dan struktur harus dikelola dengan tangan. Serde membagi solusi menjadi tiga peran: trait Serialize dan Deserialize sebagai kontrak, serde_json sebagai implementasi format JSON, dan serde_derive sebagai penulis implementasi otomatis lewat attribute #[derive]. Satu putaran to_string dan from_str menunjukkan round-trip yang bersih tanpa satu pun escape manual. Pertanyaan berikutnya: apa sebenarnya yang ditulis #[derive(Serialize, Deserialize)] di balik layar, dan apa yang terjadi kalau attribute tersebut lupa dipasang? Chapter berikutnya membuka kap mesinnya.