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:
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:
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:
- Baris 7 (
#[serde(default)]): attribute pada fielddiskonmengubah statusnya dari wajib menjadi opsional — hanya untuk proses deserialization. - 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 galatmissing field `diskon`persis sepertistokpada Chapter sebelumnya; dengan attribute, deserializer memanggilu32::default()yang menghasilkan0. - Baris 17 (
let produk2: Produk = serde_json::from_str(dengan_diskon).unwrap();): teks kedua menyertakan"diskon": 20, dan nilai dari JSON menang — attributedefaulthanya 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:
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:
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:
- 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. - Baris 11 (
#[serde(default = "diskon_bawaan")]): nilai attribute berupa string berisi nama fungsi, bukan pemanggilan fungsi. Derive memasukkan pemanggilandiskon_bawaan()ke kode yang di-generate, sehingga fungsi dieksekusi setiap kali kuncidiskonabsen dari JSON. - Baris 19 (
let harga_akhir = produk.harga * (100 - produk.diskon as u64) / 100;): nilai bawaan10langsung terpakai dalam perhitungan —15000 * 90 / 100menghasilkan13500tanpa satu punifuntuk memeriksa kelengkapan data. - Baris 23 (
let produk2: Produk = ...): JSON yang menyertakan"diskon": 25tetap menang atas nilai bawaan — aturan “nilai dari JSON selalu diutamakan” berlaku sama untuk kedua bentuk attributedefault.
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:
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:
{
"nama": "Kopi Torabika",
"harga": 15000,
"kategori": "Minuman",
"rating": 4.5
}
Kopi Torabika | Minuman | rating 4.5Empat kunci tampil rata dalam satu object, lalu struct bersarang terbangun kembali sempurna. Mari kita bedah kodenya:
- Baris 13 (
#[serde(flatten)]): attribute pada fieldmetadatamemerintahkan kedua derive —SerializedanDeserialize— untuk mengangkat isiMetadatake level atas object JSON. - 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. - Baris 30 (
let balik: Produk = serde_json::from_str(&teks).unwrap();): arah deserialize bekerja terbalik — kunci"kategori"dan"rating"yang tidak cocok dengan field milikProdukdialirkan ke structMetadata, lalu struct tersebut disusun kembali menjadi fieldbalik.metadata. - Baris 31 (
println!("{} | {} | rating {}", ...)): kode Rust tetap mengaksesbalik.metadata.kategoridua 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.