Chapter Parsing dan Serialize JSON menutup dengan satu kekakuan: field yang absen dari JSON ditolak mentah-mentah dengan galat missing field. Kekakuan tersebut menyenangkan ketika kelengkapan data memang mutlak, tetapi nyata di lapangan data berkembang seiring waktu — API versi lama belum mengenal field diskon, berkas lama pelanggan ditulis sebelum aturan baru ada, dan pembuat JSON sebelah sana tidak bisa kita paksa mengirim field yang belum dikenal sistemnya. Tekanan kedua datang dari bentuk struktur: katalog internal kita memisahkan atribut produk ke struct Metadata tersendiri agar kode rapi, sementara kontrak JSON pihak lain menginginkan semua kunci dalam satu object rata tanpa tingkatan. Dua attribute serde menyelesaikan keduanya tanpa merusak desain struct: default melunakkan kewajiban field, dan flatten meratakan struct bersarang pada saat data melintasi batas JSON.

default: Field yang Boleh Absen #

Atribut default dipasang pada field untuk menyatakan: kalau kunci ini tidak ditemukan di JSON, pakai nilai Default milik tipenya daripada gagal. Perilaku tersebut kita amati dari dua arah — JSON tanpa field diskon dan JSON yang menyertakannya:

rust
use serde::Deserialize;

#[derive(Deserialize)]
struct Produk {
    nama: String,
    harga: u64,
    #[serde(default)]
    diskon: u32,
}

fn main() {
    let tanpa_diskon = r#"{"nama": "Kopi Torabika", "harga": 15000}"#;
    let produk1: Produk = serde_json::from_str(tanpa_diskon).unwrap();
    println!("{} Rp{} | diskon: {}%", produk1.nama, produk1.harga, produk1.diskon);

    let dengan_diskon = r#"{"nama": "Kopi Torabika", "harga": 15000, "diskon": 20}"#;
    let produk2: Produk = serde_json::from_str(dengan_diskon).unwrap();
    println!("{} Rp{} | diskon: {}%", produk2.nama, produk2.harga, produk2.diskon);
}

Output yang Dihasilkan:

text
Kopi Torabika Rp15000 | diskon: 0%
Kopi Torabika Rp15000 | diskon: 20%

Dua baris keluaran tersebut menceritakan dua kejadian yang berbeda. Mari kita bedah baris-baris pentingnya:

  1. Baris 7 (#[serde(default)]): attribute pada field diskon mengubah statusnya dari wajib menjadi opsional — hanya untuk proses deserialization.
  2. Baris 13 (let produk1: Produk = serde_json::from_str(tanpa_diskon).unwrap();): teks JSON pertama tidak memuat kunci "diskon". Tanpa attribute, pemanggilan ini berakhir dengan galat missing field `diskon` persis seperti stok pada Chapter sebelumnya; dengan attribute, deserializer memanggil u32::default() yang menghasilkan 0.
  3. Baris 17 (let produk2: Produk = serde_json::from_str(dengan_diskon).unwrap();): teks kedua menyertakan "diskon": 20, dan nilai dari JSON menang — attribute default hanya bekerja saat kunci absen, bukan menimpa nilai yang ada.

Satu catatan penting: default hanya melunakkan arah baca. Saat struct seperti ini di-serialize, field diskon tetap ditulis ke JSON apa pun nilainya. Field bertipe Option<T> punya perlakuan istimewah tersendiri — tanpa attribute apa pun, Option<u32> sudah dianggap opsional: kunci absen menghasilkan None, dan nilai eksplisit null juga menghasilkan None.

default dengan Nilai Kustom #

Nilai bawaan 0 cocok untuk banyak kasus, tetapi kadang nilai absen bermakna bisnis yang lebih spesifik — diskon standar anggota toko misalnya 10, bukan nol. Attribute default = "nama_fungsi" memungkinkan kita menunjuk fungsi penghasil nilai bawaan:

rust
use serde::Deserialize;

fn diskon_bawaan() -> u32 {
    10
}

#[derive(Deserialize)]
struct Produk {
    nama: String,
    harga: u64,
    #[serde(default = "diskon_bawaan")]
    diskon: u32,
}

fn main() {
    let teks_json = r#"{"nama": "Kopi Torabika", "harga": 15000}"#;
    let produk: Produk = serde_json::from_str(teks_json).unwrap();

    let harga_akhir = produk.harga * (100 - produk.diskon as u64) / 100;
    println!("{} diskon {}% harga akhir: {}", produk.nama, produk.diskon, harga_akhir);

    let teks_dengan_diskon = r#"{"nama": "Kopi Torabika", "harga": 15000, "diskon": 25}"#;
    let produk2: Produk = serde_json::from_str(teks_dengan_diskon).unwrap();
    println!("{} diskon {}%", produk2.nama, produk2.diskon);
}

Output yang Dihasilkan:

text
Kopi Torabika diskon 10% harga akhir: 13500
Kopi Torabika diskon 25%

Field yang absen kini terisi 10 dari fungsi, dan harga dihitung dengan diskon tersebut. Mari kita bedah baris-baris pentingnya:

  1. Baris 3-5 (fn diskon_bawaan() -> u32 { 10 }): fungsi bawaan ditulis sebagai fungsi Rust biasa tanpa parameter, mengembalikan nilai yang bertipe sama persis dengan field yang dituju — u32.
  2. Baris 11 (#[serde(default = "diskon_bawaan")]): nilai attribute berupa string berisi nama fungsi, bukan pemanggilan fungsi. Derive memasukkan pemanggilan diskon_bawaan() ke kode yang di-generate, sehingga fungsi dieksekusi setiap kali kunci diskon absen dari JSON.
  3. Baris 19 (let harga_akhir = produk.harga * (100 - produk.diskon as u64) / 100;): nilai bawaan 10 langsung terpakai dalam perhitungan — 15000 * 90 / 100 menghasilkan 13500 tanpa satu pun if untuk memeriksa kelengkapan data.
  4. Baris 23 (let produk2: Produk = ...): JSON yang menyertakan "diskon": 25 tetap menang atas nilai bawaan — aturan “nilai dari JSON selalu diutamakan” berlaku sama untuk kedua bentuk attribute default.

flatten: Meratakan Struct Bersarang #

Atribut flatten mengubah batas antar struct pada sisi JSON. Struct Produk menyimpan metadata di struct Metadata terpisah, tetapi kunci "kategori" dan "rating" tampil sejajar dengan "nama" dan "harga" di JSON keluaran:

rust
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct Metadata {
    kategori: String,
    rating: f64,
}

#[derive(Serialize, Deserialize)]
struct Produk {
    nama: String,
    harga: u64,
    #[serde(flatten)]
    metadata: Metadata,
}

fn main() {
    let kopi = Produk {
        nama: String::from("Kopi Torabika"),
        harga: 15000,
        metadata: Metadata {
            kategori: String::from("Minuman"),
            rating: 4.5,
        },
    };

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

    let balik: Produk = serde_json::from_str(&teks).unwrap();
    println!("{} | {} | rating {}", balik.nama, balik.metadata.kategori, balik.metadata.rating);
}

Output yang Dihasilkan:

text
{
  "nama": "Kopi Torabika",
  "harga": 15000,
  "kategori": "Minuman",
  "rating": 4.5
}
Kopi Torabika | Minuman | rating 4.5

Empat kunci tampil rata dalam satu object, lalu struct bersarang terbangun kembali sempurna. Mari kita bedah kodenya:

  1. Baris 13 (#[serde(flatten)]): attribute pada field metadata memerintahkan kedua derive — Serialize dan Deserialize — untuk mengangkat isi Metadata ke level atas object JSON.
  2. Baris 27 (let teks = serde_json::to_string_pretty(&kopi).unwrap();): arah serialize menuliskan empat kunci rata: "nama", "harga", "kategori", "rating" — tidak ada object bersarang bernama "metadata" di JSON.
  3. Baris 30 (let balik: Produk = serde_json::from_str(&teks).unwrap();): arah deserialize bekerja terbalik — kunci "kategori" dan "rating" yang tidak cocok dengan field milik Produk dialirkan ke struct Metadata, lalu struct tersebut disusun kembali menjadi field balik.metadata.
  4. Baris 31 (println!("{} | {} | rating {}", ...)): kode Rust tetap mengakses balik.metadata.kategori dua tingkat — kerapian struct di dalam program dan kerataan JSON di luar program tercapai bersamaan.

Perilaku terhadap kunci asing juga patut dicatat. Deserializer serde bawaan mengabaikan kunci JSON yang tidak dikenal struct — kunci "aneh" pada teks masukan tidak memicu galat, baik pada struct polos maupun pada struct dengan flatten. Attribute flatten memang hanya mengubah perutean: kunci yang cocok dengan field milik outer struct diambil oleh outer struct, sisanya dialirkan ke inner struct, dan kunci yang tidak cocok dengan keduanya berakhir terabaikan. Bila kehadiran kunci asing justru perlu ditolak keras, attribute container #[serde(deny_unknown_fields)] memerintahkan deserializer memunculkan galat unknown field atas setiap kunci yang tidak dikenal — penjaga tambahan yang berguna saat kontrak JSON harus dipatuhi ketat.

Ringkasan #

Atribut default melunakkan kewajiban field saat deserialization: kunci yang absen diisi nilai Default milik tipenya, sedangkan nilai dari JSON selalu diutamakan bila hadir. Bentuk default = "nama_fungsi" mengisi nilai absen dengan hasil fungsi kustom, dan field bertipe Option<T> memang sudah opsional tanpa attribute. Atribut flatten meratakan struct bersarang di batas JSON — serialize mengangkat kunci inner struct ke level atas, deserialize mengalirkan kunci yang tidak dikenal outer struct kembali ke inner struct. Dengan empat attribute dari dua Chapter terakhir — rename, skip, default, flatten — kontrak JSON eksternal bisa kita patuhi tanpa mengorbankan desain internal Rust. Satu dunia masih tersisa: JSON yang bentuknya sama sekali tidak bisa kita ramalkan sejak awal, dan serde_json::Value adalah jawabannya di Chapter penutup section ini.