Meng-serialize struct ke JSON terasa menyenangkan karena datanya berasal dari kode kita sendiri — tipe sudah diperiksa kompilator sejak awal. Arah sebaliknya jauh lebih menegangkan: teks JSON yang masuk ke program berasal dari berkas konfigurasi yang disunting tangan, respons API milik pihak lain, atau payload antrean pesan — sumber yang sama sekali tidak bisa kita percaya begitu saja. Deserialization adalah titik di mana dunia teks yang liar dipaksa tunduk pada tipe Rust, dan proses penundukan tersebut menghasilkan tiga jenis keluhan yang khas: sintaks JSON rusak, field hilang, dan tipe data salah. Chapter ini mempraktikkan from_str pada JSON yang sehat, lalu memancing ketiga jenis keluhan tersebut satu per satu untuk membaca pesan galat aslinya, dan ditutup dengan pilihan format keluaran to_string versus to_string_pretty.
from_str: Teks JSON Menjadi Struct #
Kita mulai dari kasus yang sehat. Teks JSON untuk satu produk akan dibaca menjadi struct Produk — cukup dengan derive Deserialize karena arah serialize tidak dipakai pada contoh ini:
use serde::Deserialize;
#[derive(Deserialize)]
struct Produk {
nama: String,
harga: u64,
stok: u32,
}
fn main() {
let teks_json = r#"{"nama": "Kopi Torabika", "harga": 15000, "stok": 24}"#;
let produk: Produk = serde_json::from_str(teks_json).unwrap();
println!("Nama : {}", produk.nama);
println!("Harga: {}", produk.harga);
println!("Stok : {}", produk.stok);
}Output yang Dihasilkan:
Nama : Kopi Torabika
Harga: 15000
Stok : 24Tiga field di dalam struct terisi rapi dari tiga pasangan kunci-nilai di teks JSON. Mari kita bedah baris-baris pentingnya:
- Baris 3 (
#[derive(Deserialize)]): hanya traitDeserializeyang dibutuhkan untuk membaca JSON, jadi deriveSerializesengaja tidak dipasang pada contoh ini — keduanya memang bisa dipilih secara terpisah. - Baris 11 (
let teks_json = r#"..."#;): raw stringr#"..."#memudahkan penulisan JSON karena tanda kutip ganda di dalamnya tidak perlu di-escape dengan\". - Baris 13 (
let produk: Produk = serde_json::from_str(teks_json).unwrap();): anotasi tipe: Produkadalah kontrak yang dibaca derive — parser memetakan kunci"nama"ke fieldnama,"harga"ke fieldharga, dan seterusnya.from_strmengembalikanResult, danunwrapdipakai karena kita tahu contoh ini pasti berhasil; pola penangananResultyang lebih sabar kita pakai pada contoh galat di bawah. - Baris 14-16 (
println! ...): ketiga field terbaca sebagai tipe Rust aslinya —produk.hargaadalahu64yang siap dihitung, bukan potongan teks.
Galat Sintaks dengan Posisi Line dan Column #
JSON yang datang dari luar bisa saja rusak strukturnya — koma berlebih, tanda kutip hilang, atau kurung yang tidak seimbang. Kesalahan klasik: tanda koma yang menempel di pasangan terakhir. Program berikut sengaja memuat JSON dengan trailing comma, lalu menangani Result dengan match agar pesan galatnya bisa kita baca, bukan sekadar panik:
use serde::Deserialize;
#[derive(Deserialize)]
struct Produk {
nama: String,
harga: u64,
stok: u32,
}
fn main() {
let teks_json = r#"{"nama": "Kopi Torabika", "harga": 15000, "stok": 24,}"#;
let hasil: Result<Produk, serde_json::Error> = serde_json::from_str(teks_json);
match hasil {
Ok(produk) => println!("Berhasil: {} Rp{} stok {}", produk.nama, produk.harga, produk.stok),
Err(galat) => println!("Gagal parse: {}", galat),
}
}Output yang Dihasilkan:
Gagal parse: trailing comma at line 1 column 54Pesan galat tersebut memuat dua jenis informasi sekaligus: jenis kesalahan (trailing comma) dan posisinya (line 1 column 54). Mari kita bedah kejadiannya:
- Baris 11 (
let teks_json = r#"..."#;): teks JSON berakhir dengan"stok": 24,}— tanda koma di kolom 53 setelah pasangan terakhir, sebelum kurung tutup. - Baris 13 (
let hasil: Result<Produk, serde_json::Error> = ...): tipe galatnya bernamaserde_json::Error, satu tipe yang dipakai untuk semua kegagalan parsing — sintaks, field, maupun tipe. - Baris 15 (
Ok(produk) => ...): cabangOkmembuktikan bahwa semua field struct terbaca utuh ketika parsing berhasil — cabang ini tidak terpilih pada contoh kali ini. - Baris 16 (
Err(galat) => println!("Gagal parse: {}", galat);): cabangErrmencetak pesan lengkap. Setelah koma di kolom 53, parser mengharapkan nama kunci berikutnya, tetapi yang ditemukan justru}di kolom 54 — itulah posisi yang dilaporkan, dan jenis galatnya dideteksi sebagai koma buntut yang tidak diharapkan.
Posisi line 1 column 54 menjadi sangat berharga ketika JSON-nya panjang dan berformat rapi: galat pada JSON banyak baris akan melaporkan nomor baris yang benar, sehingga kita langsung melompat ke lokasi masalah tanpa membaca seluruh teks.
Field Hilang di JSON #
Kasus kedua: JSON-nya valid secara sintaks, tetapi isinya kurang. Rekan tim mungkin lupa menyertakan field stok saat menulis berkas data. Deserializer serde bersikap tegas — setiap field struct dianggap wajib hadir:
use serde::Deserialize;
#[derive(Deserialize)]
struct Produk {
nama: String,
harga: u64,
stok: u32,
}
fn main() {
let teks_json = r#"{"nama": "Kopi Torabika", "harga": 15000}"#;
let hasil: Result<Produk, serde_json::Error> = serde_json::from_str(teks_json);
match hasil {
Ok(produk) => println!("Berhasil: {} Rp{} stok {}", produk.nama, produk.harga, produk.stok),
Err(galat) => println!("Gagal parse: {}", galat),
}
}Output yang Dihasilkan:
Gagal parse: missing field `stok` at line 1 column 41Pesan missing field `stok` menyebut nama field yang absen secara persis, dan kolom 41 menunjuk kurung tutup } — titik di mana parser menutup object dan baru pada saat itu serde menyadari field stok tidak pernah muncul. Keketatan ini disengaja: daripada mengisi field dengan nilai karungan diam-diam, serde memilih menolak total agar kejanggalan data terlihat sejak awal. Ketika sebuah field memang pantas absen — misalnya diskon yang boleh tidak diisi — solusinya bukan melonggarkan semua field, melainkan attribute #[serde(default)] yang akan kita bahas pada Chapter Atribut default dan flatten.
Tipe Field yang Salah #
Kasus ketiga melibatkan JSON yang lengkap secara sintaks dan field, tetapi salah tipe: nilai harga dikirim sebagai string "lima belas" padahal struct menuntut u64. Inilah momen ketika sistem tipe Rust menampar data masukan:
use serde::Deserialize;
#[derive(Deserialize)]
struct Produk {
nama: String,
harga: u64,
stok: u32,
}
fn main() {
let teks_json = r#"{"nama": "Kopi Torabika", "harga": "lima belas", "stok": 24}"#;
let hasil: Result<Produk, serde_json::Error> = serde_json::from_str(teks_json);
match hasil {
Ok(produk) => println!("Berhasil: {} Rp{} stok {}", produk.nama, produk.harga, produk.stok),
Err(galat) => println!("Gagal parse: {}", galat),
}
}Output yang Dihasilkan:
Gagal parse: invalid type: string "lima belas", expected u64 at line 1 column 47Pesan tersebut menyebut nilai yang bermasalah (string "lima belas"), tipe yang diharapkan (u64), dan posisi kolom 47 — tepat di tanda kutip penutup nilai "lima belas", titik di mana parser selesai membaca string dan menyerahkannya ke deserializer u64 yang menolak. Perlu dicatat bahwa JSON sendiri tidak melarang string pada posisi tersebut — format JSON hanya peduli sintaks. Pemeriksaan yang menolak "lima belas" berasal dari anotasi tipe Produk di sisi Rust, bukan dari aturan JSON. Justru karena itulah deserialization serde berharga: data liar dari luar diperiksa terhadap skema struct kita sebelum sempat meracau di dalam program.
Ringkas atau Rapi #
Setelah arah baca beres, kita kembali ke arah tulis untuk satu keputusan praktis: format keluaran. Fungsi to_string menghasilkan JSON ringkas tanpa spasi berlebih, sedangkan to_string_pretty menyisipkan indentasi dan baris baru:
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 ringkas = serde_json::to_string(&kopi).unwrap();
let rapi = serde_json::to_string_pretty(&kopi).unwrap();
println!("Ringkas: {}", ringkas);
println!("Rapi:");
println!("{}", rapi);
}Output yang Dihasilkan:
Ringkas: {"nama":"Kopi Torabika","harga":15000,"stok":24}
Rapi:
{
"nama": "Kopi Torabika",
"harga": 15000,
"stok": 24
}Kedua hasil tersebut berasal dari implementasi Serialize yang sama persis — perbedaannya murni soal penataan teks. JSON ringkas menghemat byte sehingga cocok untuk pertukaran antar mesin: payload HTTP, antrean pesan, dan baris log. JSON rapi memudahkan mata manusia sehingga cocok untuk berkas konfigurasi, laporan, dan hasil eksekusi manual di terminal.
Selain to_string dan from_str, keluarga fungsi serde_json punya varian sumber dan tujuan lain yang memakai kontrak Serialize/Deserialize yang sama: from_slice membaca dari &[u8] untuk data biner dari jaringan, from_reader membaca dari apa pun yang mengimplementasikan std::io::Read seperti File tanpa memuat seluruh isi ke memori, dan to_writer menulis langsung ke tujuan berbasis Write. Seluruh varian tersebut melaporkan galat dengan tipe serde_json::Error yang sama dengan yang kita bedah di atas.
Ringkasan #
from_str membaca teks JSON menjadi struct dengan bantuan derive Deserialize, dan anotasi tipe menjadi skema yang dipakai untuk memvalidasi data masukan. Tiga jenis kegagalan nyata menghasilkan pesan yang masing-masing informatif: trailing comma at line 1 column 54 untuk sintaks rusak, missing field `stok` untuk field absen, dan invalid type: string "lima belas", expected u64 untuk tipe salah — semuanya dilengkapi posisi line dan column. Arah tulis menawarkan dua gaya: to_string yang ringkas untuk mesin dan to_string_pretty yang berindentasi untuk manusia. Keketatan validasi field kadang menjadi penghalang ketika data memang boleh tidak lengkap atau penamaannya harus menyesuaikan kontrak pihak lain — dua atribut serde berikutnya, rename/skip dan default/flatten, mekanismenya kita bahas mulai Chapter berikutnya.