Perilaku bawaan serde — nama field menjadi kunci JSON apa adanya — nyaman selama kita mengurus data sendiri. Dua tekanan dunia nyata segera merusak kenyamanan tersebut. Pertama, konvensi penamaan: komunitas Rust memakai snake_case untuk field, sementara API web kebanyakan bertukar kunci bergaya camelCase, dan kedua pihak tidak mau berpindah keyakinan. Kedua, kerahasiaan: struct AkunPengguna menyimpan field password untuk keperluan internal program, tetapi nilai tersebut sama sekali tidak boleh ikut tercetak ketika struct di-serialize untuk dikirim ke jaringan. Mengganti nama field di struct atau membuang field dari struct bukanlah solusi — keduanya merusak kode Rust yang sudah ada. Serde menyediakan attribute #[serde(...)] yang menyesuaikan perilaku serialize/deserialize tanpa menyentuh definisi struct sama sekali. Chapter ini membahas dua attribute pertama: rename dengan rename_all untuk penamaan, dan skip untuk penyembunyian field.

rename_all: Menyesuaikan Konvensi Penamaan #

Atribut rename_all dipasang pada struct — posisi tersebut dinamai container attribute karena memberlakukan aturan untuk seluruh isi struct. Nilai "camelCase" memerintahkan serde mengubah semua nama field bergaya snake_case menjadi camelCase pada JSON. Pesanan pembelian dari sistem web modern menjadi contoh yang pas:

rust
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct DetailPesanan {
    nama_produk: String,
    harga_satuan: u64,
    jumlah_item: u32,
}

fn main() {
    let pesanan = DetailPesanan {
        nama_produk: String::from("Kopi Torabika"),
        harga_satuan: 15000,
        jumlah_item: 2,
    };

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

    let balik: DetailPesanan = serde_json::from_str(&teks).unwrap();
    println!("Total bayar: {}", balik.harga_satuan * balik.jumlah_item as u64);
}

Output yang Dihasilkan:

text
{
  "namaProduk": "Kopi Torabika",
  "hargaSatuan": 15000,
  "jumlahItem": 2
}
Total bayar: 30000

Field Rust tetap nama_produk dengan garis bawah, sementara kunci JSON menjadi namaProduk dengan huruf besar di tengah. Mari kita bedah baris-baris pentingnya:

  1. Baris 4 (#[serde(rename_all = "camelCase")]): attribute container ini ditempel di antara derive dan definisi struct, memberlakukan konversi nama untuk semua field sekaligus — serialize dan deserialize sama-sama mengikuti.
  2. Baris 5-9 (struct DetailPesanan { ... }): nama field tetap memakai konvensi Rust snake_case; kode internal kita tidak perlu berubah gaya hanya karena pihak lain berbeda selera.
  3. Baris 21 (let balik: DetailPesanan = serde_json::from_str(&teks).unwrap();): round-trip terbukti dua arah — teks dengan kunci namaProduk berhasil di-parse kembali ke field nama_produk, karena pemetaan rename berlaku simetris.
  4. Baris 22 (println!("Total bayar: {}", ...)): hasil 30000 membuktikan balik.harga_satuan terbaca sebagai u64 dan ikut perhitungan aritmetika, bukan sekadar teks.

Selain "camelCase", nilai lain yang sering dipakai antara lain "kebab-case" (nama-produk), "SCREAMING_SNAKE_CASE" (NAMA_PRODUK), dan "PascalCase" (NamaProduk) — semuanya mengikuti konvensi penamaan yang berlaku di sistem tujuan.

rename untuk Satu Field #

Kadang ketidakcocokan nama hanya menimpa satu field — misalnya API lama yang sudah terlanjur memakai kunci "desc" yang singkat, sementara kita ingin field Rust bernama deskripsi yang utuh. Atribut rename ditempel pada field tersebut, bukan pada struct:

rust
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct Produk {
    nama: String,
    #[serde(rename = "desc")]
    deskripsi: String,
}

fn main() {
    let kopi = Produk {
        nama: String::from("Kopi Torabika"),
        deskripsi: String::from("Robusta dengan proses giling halus"),
    };

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

    let balik: Produk = serde_json::from_str(&teks).unwrap();
    println!("Field Rust tetap 'deskripsi': {}", balik.deskripsi);
}

Output yang Dihasilkan:

text
Hasil serialize:
{
  "nama": "Kopi Torabika",
  "desc": "Robusta dengan proses giling halus"
}
Field Rust tetap 'deskripsi': Robusta dengan proses giling halus

Kunci "desc" muncul di JSON, sementara kode Rust tetap memanggil balik.deskripsi. Mari kita bedah kodenya:

  1. Baris 6 (#[serde(rename = "desc")]): field attribute ini mengganti nama kunci untuk field deskripsi saja; field nama tetap memakai nama aslinya.
  2. Baris 16 (let teks = serde_json::to_string_pretty(&kopi).unwrap();): hasil serialize memuat kunci "desc" — kontrak JSON mengikuti attribute, bukan nama field Rust.
  3. Baris 20 (let balik: Produk = serde_json::from_str(&teks).unwrap();): arah deserialize memakai pemetaan yang sama — parser mencari kunci "desc" di JSON dan mengisinya ke field deskripsi. Kunci "deskripsi" di JSON justru akan diabaikan.

skip: Menyembunyikan Field dari JSON #

Atribut skip menjawab masalah kerahasiaan. Field password tetap hidup di struct dan dipakai kode internal, tetapi serde seolah tidak melihatnya ketika menulis dan membaca JSON:

rust
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct AkunPengguna {
    nama: String,
    email: String,
    #[serde(skip)]
    password: String,
}

fn main() {
    let akun = AkunPengguna {
        nama: String::from("Budi Santoso"),
        email: String::from("[email protected]"),
        password: String::from("sandi-contoh-123"),
    };

    let teks = serde_json::to_string_pretty(&akun).unwrap();
    println!("JSON yang dikirim ke jaringan:");
    println!("{}", teks);

    let hasil: AkunPengguna = serde_json::from_str(&teks).unwrap();
    println!("Nama    : {}", hasil.nama);
    println!("Password: '{}'", hasil.password);
}

Output yang Dihasilkan:

text
JSON yang dikirim ke jaringan:
{
  "nama": "Budi Santoso",
  "email": "[email protected]"
}
Nama    : Budi Santoso
Password: ''

JSON keluaran hanya memuat dua field — password lenyap dari teks yang berkelana ke jaringan. Mari kita bedah baris-baris pentingnya:

  1. Baris 7 (#[serde(skip)]): field attribute ini mengeluarkan field password dari kedua arah: implementasi Serialize tidak menulisnya, dan implementasi Deserialize tidak mencarinya di JSON.
  2. Baris 15 (password: String::from("sandi-contoh-123")): nilai sandi tetap tersimpan di struct di memori — field Rust dan seluruh kode internal tidak terpengaruh attribute ini.
  3. Baris 18 (let teks = serde_json::to_string_pretty(&akun).unwrap();): hasil JSON berhenti di field email; tidak ada kunci "password" sama sekali, sehingga nilai sandi tidak pernah meninggalkan program.
  4. Baris 22 (let hasil: AkunPengguna = serde_json::from_str(&teks).unwrap();): parse balik berhasil meskipun JSON tidak memuat password — field yang di-skip tidak dianggap hilang, berbeda dengan field biasa yang memicu galat missing field dari Chapter sebelumnya.
  5. Baris 24 (println!("Password: '{}'", hasil.password);): field terisi string kosong ''. Karena JSON tidak memuat data untuk field ini, deserializer mengisinya dengan Default::default() milik tipe field — String punya implementasi Default berupa string kosong. Tipe field yang di-skip memang harus mengimplementasikan Default agar strategi pengisian ini sah.

Atribut satu arah juga tersedia untuk kebutuhan yang lebih halus: skip_serializing hanya menyembunyikan saat menulis JSON, skip_deserializing hanya mengabaikan saat membaca. Pola yang paling sering dipakai di proyek nyata adalah skip_serializing_if = "Option::is_none" pada field Option<T> — field yang bernilai None tidak ditulis ke JSON, namun tetap sah absen saat parsing.

Ringkasan #

Atribut rename_all = "camelCase" menyesuaikan seluruh nama field ke konvensi JSON pihak lain tanpa mengubah gaya penamaan Rust, dan rename mengganti nama kunci untuk satu field tertentu — keduanya bekerja simetris pada serialize dan deserialize. Atribut skip mengeluarkan field dari JSON di kedua arah: nilai tidak pernah tertulis, dan field diisi nilai Default milik tipenya saat parsing. Keluarga skip yang lebih halus seperti skip_serializing_if menangani kasus satu arah. Pemetaan nama dan visibilitas kini terkendali, namun dua kekakuan lain masih menunggu: field yang wajib hadir padahal data kadang tidak lengkap, dan struct bersarang yang harus tampil rata di JSON. Keduanya dijawab attribute default dan flatten pada Chapter berikutnya.