Pada Chapter sebelumnya, satu baris #[derive(Serialize, Deserialize)] mengubah struct Tugas biasa menjadi data yang bisa berpindah dua arah lewat JSON. Baris tersebut terasa seperti sulap, padahal tidak ada sihir sama sekali: attribute derive adalah proc macro yang menuliskan implementasi trait Serialize dan Deserialize untuk kita pada saat kompilasi. Memahami mekanisme tersebut penting karena dua alasan. Pertama, pesan galat kompilator akan menyebut trait Serialize secara gamblang saat attribute derive lupa dipasang, dan kita perlu bisa membacanya. Kedua, keyakinan bahwa derive “hanya” menulis kode Rust biasa membuat kita berhenti memperlakukan serde sebagai kotak hitam. Chapter ini membuka kap mesin derive, menangkap galat asli saat derive terlewat, lalu memakainya pada struct Produk untuk toko kopi kita.

Trait sebagai Kontrak yang Harus Diimplementasikan #

Fungsi serde_json::to_string sebenarnya punya signature generik dengan syarat: parameter T-nya harus mengimplementasikan trait Serialize. Syarat tersebut dinamai trait bound, dan kompilator memeriksa kepatuhannya sebelum program boleh dikompilasi. Struct Produk yang kita definisikan sendiri tidak otomatis mengimplementasikan trait apa pun selain yang kita tulis — sehingga kode berikut pasti ditolak:

rust
struct Produk {
    nama: String,
    harga: u64,
    stok: u32,
}

fn main() {
    let kopi = Produk {
        nama: String::from("Kopi Torabika"),
        harga: 15000,
        stok: 24,
    };

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

Pesan galat kompilator untuk kode tersebut (potongan intinya; daftar panjang tipe bawaan yang mengimplementasikan Serialize kita ringkas):

text
error[E0277]: the trait bound `Produk: serde::Serialize` is not satisfied
   --> src/main.rs:14:38
    |
 14 |     let teks = serde_json::to_string(&kopi).unwrap();
    |                --------------------- ^^^^^ unsatisfied trait bound
    |                |
    |                required by a bound introduced by this call
    |
help: the trait `serde_core::ser::Serialize` is not implemented for `Produk`
   --> src/main.rs:1:1
    |
   1 | struct Produk {
    | ^^^^^^^^^^^^^
    = note: for local types consider adding `#[derive(serde::Serialize)]` to your `Produk` type
    = note: for types from other crates check whether the crate offers a `serde` feature flag
    = help: the following other types implement trait `serde_core::ser::Serialize`:
              ...
            and 128 others
    ...

For more information about this error, try `rustc --explain E0277`.

Baris ... pada potongan di atas menandai bagian yang kita ringkas: daftar tipe bawaan seperti &'a T hingga tuple (T0, T1, T2, T3, T4), serta blok note: required by a bound in serde_json::to_string yang menunjuk kode sumber serde_json. Galat E0277 tersebut adalah kabar baik, bukan kabar buruk. Pesannya menyebut tiga hal secara berurutan: trait mana yang tidak terpenuhi (Produk: serde::Serialize), pemanggilan mana yang menuntutnya (to_string(&kopi)), dan perbaikan yang disarankan — tambahkan #[derive(serde::Serialize)] pada struct lokal kita. Sebutan serde_core::ser::Serialize pada bagian help merujuk crate internal tempat serde modern menyimpan definisi trait; trait yang dimaksud tetap Serialize yang sama. Kompilator juga mengingatkan bahwa tipe bawaan seperti String dan u64 sudah mengimplementasikan Serialize — hanya struct buatan kita yang belum.

Derive Macro Menulis Kode untuk Kita #

Attribute derive mengisi kekosongan tersebut dengan cara yang sama seperti #[derive(Debug)] atau #[derive(Clone)] yang sudah kita kenal pada Chapter Macros: sebuah proc macro menerima token struct sebagai masukan, lalu menghasilkan token kode tambahan sebelum tahap pemeriksaan tipe. Kode hasil ekspansi untuk Produk pada dasarnya berbentuk seperti ini — ilustrasi yang diringkas, bukan hasil verbatim:

rust
// Ilustrasi bentuk kode yang di-generate (diringkas, bukan hasil verbatim)
impl serde::Serialize for Produk {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        // menulis setiap field sebagai anggota object:
        // "nama" (String), "harga" (u64), "stok" (u32)
    }
}

Implementasi tersebut adalah Rust biasa — kode yang bisa kita tulis sendiri dengan tangan, hanya saja membosankan dan rawan salah ketika struct berubah. Derive mengambil definisi struct terbaru pada setiap kompilasi, sehingga field baru otomatis ikut ter-serialize tanpa penyuntingan manual. Pemisahan ini menjelaskan pembagian tugas dari Chapter sebelumnya: crate serde menyediakan trait Serialize dan Deserialize sebagai kontrak, crate serde_derive menghasilkan implementasinya, dan crate serde_json menjalankan kontrak tersebut untuk format JSON.

Kedua trait bisa di-derive secara terpisah. Struct yang hanya akan ditulis keluar cukup memakai #[derive(Serialize)]; struct yang hanya akan dibaca dari JSON cukup memakai #[derive(Deserialize)]. Kebiasaan yang sehat adalah memasang keduanya pada tipe domain seperti Produk, karena data bisnis hampir selalu bergerak dua arah.

Contoh Lengkap: struct Produk dengan to_string_pretty #

Sekarang kita rakit contoh utuhnya. Struct Produk memakai derive lengkap, dan fungsi to_string_pretty dipilih agar hasil JSON enak dibaca manusia:

rust
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct Produk {
    nama: String,
    harga: u64,
    stok: u32,
}

fn main() {
    let kopi = Produk {
        nama: String::from("Kopi Torabika"),
        harga: 15000,
        stok: 24,
    };

    let teks = serde_json::to_string_pretty(&kopi).unwrap();
    println!("{}", teks);
}

Output yang Dihasilkan:

text
{
  "nama": "Kopi Torabika",
  "harga": 15000,
  "stok": 24
}

Teks JSON tersebut memuat tiga kunci yang persis mengikuti nama field struct, dengan nilai yang mengikuti tipe masing-masing field. Mari kita bedah baris-baris pentingnya:

  1. Baris 1 (use serde::{Deserialize, Serialize};): mengimpor trait Serialize dan Deserialize beserta macro derive keduanya dalam satu statement.
  2. Baris 3 (#[derive(Serialize, Deserialize)]): proc macro membaca definisi Produk dan menghasilkan dua blok impl — satu untuk Serialize, satu untuk Deserialize — pada saat kompilasi. Tanpa baris ini, kode akan ditolak dengan E0277 seperti yang kita tangkap di atas.
  3. Baris 4-8 (struct Produk { ... }): definisi struct memakai tipe sederhana: String untuk nama, u64 untuk harga, dan u32 untuk stok. Ketiga tipe tersebut sudah mengimplementasikan Serialize dan Deserialize, sehingga derive tinggal merangkainya.
  4. Baris 17 (let teks = serde_json::to_string_pretty(&kopi).unwrap();): pemanggilan memakai reference &kopi — data tidak dipindahkan kepemilikannya, dan unwrap mengambil String dari Result karena serialize struct sederhana ini tidak mungkin gagal.
  5. Baris 18 (println!("{}", teks);): to_string_pretty menghasilkan JSON berindentasi dua spasi dengan baris terpisah tiap field — format yang tepat untuk berkas konfigurasi yang akan dibaca manusia.

Pemetaan Tipe Rust ke JSON #

Derive menerjemahkan tipe Rust ke tipe JSON mengikuti aturan tetap. Pemetaan untuk tipe yang kita pakai di section ini:

Tipe RustBentuk JSONContoh hasil
Stringstring"Kopi Torabika"
u64, u32, i32number15000
f64number4.5
boolbooleantrue
Option<T>nilai T atau nullnull
Vec<T>array["padat", "kemasan-250g"]

Aturan pada tabel tersebut mengikuti data model JSON yang terbatas: JSON hanya mengenal string, number, boolean, array, object, dan null — tidak ada pembedaan bilangan bulat dan pecahan, dan tidak ada char. Serde menerjemahkan u64 dan f64 ke number yang sama, sehingga angka 15000 dan 4.5 sama-sama sah sebagai number JSON. Nama field struct menjadi kunci object tanpa perubahan: field nama menjadi kunci "nama" dengan tepat. Chapter Atribut rename dan skip akan menunjukkan cara melanggar aturan nama tersebut secara terkendali, misalnya saat API eksternal menuntut konvensi camelCase.

Ringkasan #

Trait Serialize dan Deserialize adalah kontrak yang diperiksa kompilator: tanpa implementasi, pemanggilan serde_json::to_string ditolak dengan E0277 lengkap dengan saran menambahkan derive. Attribute #[derive(Serialize, Deserialize)] menjalankan proc macro yang menuliskan blok impl pada saat kompilasi — kode Rust biasa yang mengikuti definisi struct terbaru. Struct Produk dengan tiga field menjadi JSON rapi lewat to_string_pretty, dan pemetaan tipe Rust ke tipe JSON berjalan mengikuti aturan tetap. Dengan struct yang bisa di-serialize, arah berikutnya adalah membaca JSON dari luar menjadi struct — termasuk menghadapi JSON yang rusak, kurang field, atau bertipe salah. Chapter Parsing dan Serialize JSON menanganinya satu per satu.