JSON dengan Serde di Rust
JSON ada di mana-mana: file konfigurasi, web API, data simpanan game. Di Rust, hampir semua orang memakai dua crate (library Rust) untuk mengolahnya. serde (singkatan dari serialize / deserialize) tahu cara mengubah nilai Rust menjadi data dan sebaliknya. serde_json mengajarinya format JSON.
Bayangkan serde sebagai penerjemah. Kamu cukup mendeskripsikan datamu sekali, sebagai struct Rust biasa, lalu serde menerjemahkannya ke JSON dan kembali lagi untukmu. Kamu tidak perlu menulis teks JSON secara manual, dan tidak perlu membongkar JSON karakter demi karakter.
Persiapan
Section titled “Persiapan”Di dalam proyekmu, tambahkan kedua crate. Feature derive membuat serde menuliskan kode penerjemahnya untukmu:
cargo add serde --features derivecargo add serde_jsonSekarang Cargo.toml kamu berisi:
[dependencies]serde = { version = "1.0", features = ["derive"] }serde_json = "1.0"Kalau Cargo.toml dan cargo add masih baru buatmu, baca dulu Cargo dan crate. Rust Playground sudah menyediakan kedua crate ini, jadi setiap tombol “Coba sendiri” di halaman ini bisa langsung dipakai tanpa persiapan.
Dari struct ke JSON
Section titled “Dari struct ke JSON”Tambahkan #[derive(Serialize)] ke sebuah struct, dan serde_json::to_string bisa mengubahnya menjadi teks JSON:
use serde::Serialize;
#[derive(Serialize)]struct User { name: String, age: u32, admin: bool,}
fn main() { let user = User { name: String::from("Ana"), age: 28, admin: false };
let compact = serde_json::to_string(&user).unwrap(); println!("{}", compact);
let pretty = serde_json::to_string_pretty(&user).unwrap(); println!("{}", pretty);}Output:
{"name":"Ana","age":28,"admin":false}{ "name": "Ana", "age": 28, "admin": false}to_string menghasilkan JSON yang ringkas, cocok untuk dikirim lewat jaringan. to_string_pretty menambahkan baris baru dan spasi, lebih enak untuk file yang akan dibaca manusia. Urutan key-nya sama dengan urutan field di struct-mu.
Dari JSON ke struct
Section titled “Dari JSON ke struct”Arah sebaliknya disebut deserializing. Tambahkan Deserialize ke daftar derive, lalu panggil serde_json::from_str. Kamu memberi tahu tipe yang kamu inginkan lewat anotasi tipe:
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]struct User { name: String, age: u32, admin: bool,}
fn main() -> Result<(), serde_json::Error> { let text = r#"{"name": "Ana", "age": 28, "admin": false}"#;
let user: User = serde_json::from_str(text)?; println!("{:?}", user); println!("{} berumur {} tahun", user.name, user.age); Ok(())}Output:
User { name: "Ana", age: 28, admin: false }Ana berumur 28 tahunAda dua hal yang perlu diperhatikan:
r#"..."#adalah raw string. Di dalamnya, kamu bebas menulis", sangat praktis untuk JSON.from_strmengembalikanResult, karena teksnya mungkin bukan JSON yang valid. Di sinimainmengembalikanResult, jadi kita bisa memakai?untuk meneruskan error ke atas.
Kalau JSON-nya salah
Section titled “Kalau JSON-nya salah”JSON di dunia nyata sering tidak sesuai harapan. Ada field yang hilang, atau angka yang datang sebagai teks. serde memeriksa semuanya dan memberimu error yang jelas, bukan membuat program crash. Pakai match untuk menanganinya:
use serde::Deserialize;
#[derive(Deserialize, Debug)]struct User { name: String, age: u32,}
fn main() { let inputs = [ r#"{"name": "Ana", "age": 28}"#, r#"{"name": "Ben"}"#, r#"{"name": "Cy", "age": "old"}"#, ];
for text in inputs { match serde_json::from_str::<User>(text) { Ok(user) => println!("OK: {:?}", user), Err(e) => println!("Error: {}", e), } }}Output:
OK: User { name: "Ana", age: 28 }Error: missing field `age` at line 1 column 15Error: invalid type: string "old", expected u32 at line 1 column 27Pesan error dari serde berbahasa Inggris. Yang pertama berarti field age tidak ada, yang kedua berarti tipenya salah: string "old" padahal yang diharapkan u32. Keduanya juga menyebutkan baris dan kolom tempat masalahnya ditemukan. (Teks JSON-nya sengaja dibiarkan dalam bahasa Inggris, karena nomor kolom di pesan error bergantung pada panjang teks.)
Bagian ::<User> disebut “turbofish”. Ini cara lain untuk menyebutkan tipe yang kamu inginkan, berguna saat tidak ada let user: User untuk menaruh anotasinya.
Key yang hilang: Option dan #[serde(default)]
Section titled “Key yang hilang: Option dan #[serde(default)]”Bagaimana kalau sebuah field memang opsional? Ada dua pilihan bagus:
- Jadikan field itu
Option. Key yang tidak ada (ataunull) menjadiNone. - Tambahkan
#[serde(default)]. Key yang tidak ada akan diisi nilai default dari tipenya:0,false,Stringkosong, atauVeckosong.
use serde::Deserialize;
#[derive(Deserialize, Debug)]struct Profile { name: String, bio: Option<String>, #[serde(default)] followers: u32, #[serde(default)] tags: Vec<String>,}
fn main() { let full = r#"{"name": "Ana", "bio": "Suka Rust", "followers": 120, "tags": ["rust"]}"#; let short = r#"{"name": "Ben"}"#;
let a: Profile = serde_json::from_str(full).unwrap(); let b: Profile = serde_json::from_str(short).unwrap();
println!("{:?}", a); println!("{:?}", b);}Output:
Profile { name: "Ana", bio: Some("Suka Rust"), followers: 120, tags: ["rust"] }Profile { name: "Ben", bio: None, followers: 0, tags: [] }Pakai Option kalau “tidak ada nilai” punya arti yang berbeda dari “nol” atau “kosong”. Pakai default kalau nilai default-nya sudah cukup.
Mengganti nama field
Section titled “Mengganti nama field”Rust memakai snake_case untuk nama field, tapi banyak web API memakai camelCase, dan ada juga yang memakai nama yang sama sekali tidak valid di Rust (seperti type). Kamu tidak perlu mengorbankan nama Rust yang rapi. Pakai rename_all untuk seluruh struct, dan rename untuk satu field:
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]#[serde(rename_all = "camelCase")]struct UserProfile { user_id: u64, display_name: String, is_verified: bool, #[serde(rename = "type")] account_type: String,}
fn main() { // contoh respons yang mungkin dikirim balik oleh web API let response = r#"{ "userId": 42, "displayName": "Ana Silva", "isVerified": true, "type": "premium" }"#;
let profile: UserProfile = serde_json::from_str(response).unwrap(); println!("{} (id {}) punya akun {}", profile.display_name, profile.user_id, profile.account_type);
// serialisasi memakai nama yang sama, jadi JSON-nya bisa bolak-balik tanpa berubah println!("{}", serde_json::to_string(&profile).unwrap());}Output:
Ana Silva (id 42) punya akun premium{"userId":42,"displayName":"Ana Silva","isVerified":true,"type":"premium"}Secara default, serde begitu saja mengabaikan key JSON yang tidak ada di struct-mu. Jadi kamu cukup mendeskripsikan field yang kamu butuhkan, meskipun API-nya mengirim lima puluh field.
Daftar data: Vec berisi struct
Section titled “Daftar data: Vec berisi struct”Array JSON menjadi Vec (vektor). Struct juga bisa berisi struct dan vektor lain, jadi kamu bisa mendeskripsikan JSON yang bertingkat cukup dengan menyusun tipe secara bersarang:
use serde::Deserialize;
#[derive(Deserialize, Debug)]struct Task { title: String, done: bool,}
fn main() { let text = r#"[ {"title": "Belajar serde", "done": true}, {"title": "Menulis file config", "done": false}, {"title": "Memanggil API", "done": false} ]"#;
let tasks: Vec<Task> = serde_json::from_str(text).unwrap();
for task in &tasks { let mark = if task.done { "x" } else { " " }; println!("[{}] {}", mark, task.title); } let left = tasks.iter().filter(|t| !t.done).count(); println!("{} tugas tersisa", left);}Output:
[x] Belajar serde[ ] Menulis file config[ ] Memanggil API2 tugas tersisaJSON tanpa tipe: Value dan json!
Section titled “JSON tanpa tipe: Value dan json!”Kadang kamu tidak tahu bentuk JSON-nya sejak awal, atau kamu hanya butuh sebagian kecil darinya. Untuk itu, pakai serde_json::Value. Tipe ini bisa menampung JSON apa pun: objek, array, string, angka, bool, atau null. Makro json! membuat Value dengan sintaks yang mirip JSON:
use serde_json::{json, Value};
fn main() { let data: Value = json!({ "city": "Lisbon", "temps": [18, 21, 19], "sunny": true });
println!("Kota: {}", data["city"]); println!("Suhu pertama: {}", data["temps"][0]);
// as_str() memberimu &str biasa, bukan string JSON if let Some(city) = data["city"].as_str() { println!("Teks biasa: {}", city); }
// key yang tidak ada menghasilkan Null, bukan crash println!("Angin: {}", data["wind"]);}Output:
Kota: "Lisbon"Suhu pertama: 18Teks biasa: LisbonAngin: nullPerhatikan bahwa mencetak data["city"] menampilkan tanda kutip: nilainya masih berupa string JSON. Pakai .as_str(), .as_i64(), atau .as_bool() untuk mendapatkan nilai Rust biasa. Masing-masing mengembalikan Option, karena nilainya bisa saja bertipe lain.
Value memang fleksibel, tapi kamu kehilangan bantuan dari compiler. Utamakan struct setiap kali kamu tahu bentuk datanya.
Membaca dan menulis file JSON
Section titled “Membaca dan menulis file JSON”Ini contoh dari dunia nyata: menyimpan pengaturan aplikasi ke config.json lalu memuatnya kembali. Contoh ini menggabungkan halaman ini dengan Membaca dan menulis file. std::fs::write menyimpan teksnya, dan std::fs::read_to_string memuatnya kembali:
use serde::{Deserialize, Serialize};use std::error::Error;use std::fs;
#[derive(Serialize, Deserialize, Debug)]struct Config { username: String, theme: String, font_size: u32,}
fn main() -> Result<(), Box<dyn Error>> { let config = Config { username: String::from("ana"), theme: String::from("dark"), font_size: 14, };
// simpan let text = serde_json::to_string_pretty(&config)?; fs::write("config.json", text)?;
// muat let loaded_text = fs::read_to_string("config.json")?; let loaded: Config = serde_json::from_str(&loaded_text)?; println!("Dimuat: {:?}", loaded); Ok(())}Output:
Dimuat: Config { username: "ana", theme: "dark", font_size: 14 }Kenapa Box<dyn Error>? main ini bisa gagal dengan dua cara yang berbeda: error file (std::io::Error) atau error JSON (serde_json::Error). Box<dyn Error> berarti “error jenis apa pun”, jadi ? bisa dipakai untuk keduanya. Halaman Error kustom menjelaskan hal ini lebih detail.
Kesalahan umum
Section titled “Kesalahan umum”Lupa men-derive Deserialize. Kalau struct-mu hanya men-derive Serialize, kamu bisa menulis JSON tapi tidak bisa membacanya. Pesan error-nya panjang, tapi baris-baris yang penting adalah:
error[E0277]: the trait bound `User: serde::Deserialize<'de>` is not satisfied |10 | let user: User = serde_json::from_str(r#"{"name":"Ana","age":28}"#).unwrap(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ unsatisfied trait bound | = note: for local types consider adding `#[derive(serde::Deserialize)]` to your `User` typeIntinya, compiler bilang User belum mengimplementasikan trait Deserialize, dan menyarankan untuk menambahkan #[derive(serde::Deserialize)]. Solusinya: #[derive(Serialize, Deserialize)]. Kalau yang kamu lihat malah “cannot find derive macro Serialize” (derive macro Serialize tidak ditemukan), berarti kamu menambahkan serde tanpa feature derive. Jalankan cargo add serde --features derive.
Memakai .unwrap() pada data yang tidak kamu kendalikan. Di contoh kecil itu tidak masalah, tapi JSON dari pengguna atau dari sebuah website bisa saja rusak. File yang rusak akan membuat programmu crash dengan panic. Pakai ? atau match seperti di atas.
Mengira data["name"] akan tercetak tanpa tanda kutip. Value dicetak sebagai JSON. Panggil .as_str() untuk mendapatkan teks biasa.
Latihan
Section titled “Latihan”1. Simpan daftar belanja
Section titled “1. Simpan daftar belanja”Buat struct Item dengan name (String) dan qty (u32). Masukkan dua item ke sebuah Vec, ubah seluruh vektornya menjadi JSON dengan satu pemanggilan, lalu cetak.
Petunjuk: hanya struct-nya yang butuh #[derive(Serialize)]. Vec yang isinya bisa diserialisasi juga bisa diserialisasi, dan hasilnya menjadi array JSON.
Lihat solusi
use serde::Serialize;
#[derive(Serialize)]struct Item { name: String, qty: u32,}
fn main() { let list = vec![ Item { name: String::from("apel"), qty: 3 }, Item { name: String::from("roti"), qty: 1 }, ];
let json = serde_json::to_string(&list).unwrap(); println!("{}", json);}Output:
[{"name":"apel","qty":3},{"name":"roti","qty":1}]serde sudah tahu cara menangani Vec, String, dan angka, jadi cukup men-derive Serialize pada Item. Coba to_string_pretty untuk melihat daftar yang sama tersebar di beberapa baris.
2. Pengaturan dengan nilai default
Section titled “2. Pengaturan dengan nilai default”Baca JSON ini ke dalam struct Settings: {"siteTitle": "Blog saya", "postsPerPage": 5}. Struct-nya harus punya site_title, posts_per_page, dan theme yang boleh tidak ada. Lalu baca juga {"siteTitle": "Catatan"}, di mana posts_per_page harus jatuh ke nilai 0. Cetak keduanya dengan {:?}.
Petunjuk: kamu butuh #[serde(rename_all = "camelCase")] pada struct, Option<String> untuk theme, dan #[serde(default)] pada posts_per_page.
Lihat solusi
use serde::Deserialize;
#[derive(Deserialize, Debug)]#[serde(rename_all = "camelCase")]struct Settings { site_title: String, #[serde(default)] posts_per_page: u32, theme: Option<String>,}
fn main() { let full = r#"{"siteTitle": "Blog saya", "postsPerPage": 5}"#; let short = r#"{"siteTitle": "Catatan"}"#;
let a: Settings = serde_json::from_str(full).unwrap(); let b: Settings = serde_json::from_str(short).unwrap();
println!("{:?}", a); println!("{:?}", b);}Output:
Settings { site_title: "Blog saya", posts_per_page: 5, theme: None }Settings { site_title: "Catatan", posts_per_page: 0, theme: None }rename_all memetakan siteTitle ke site_title dan postsPerPage ke posts_per_page. Kedua JSON tidak punya theme, jadi nilainya None di keduanya, dan postsPerPage yang hilang di JSON kedua menjadi 0. Hapus #[serde(default)], dan unwrap() yang kedua akan panic dengan pesan “missing field postsPerPage” (field postsPerPage tidak ada). .unwrap() tidak masalah di sini karena kita sendiri yang menulis JSON-nya; untuk input sungguhan, pakai ? atau match.
Ringkasan
Section titled “Ringkasan”- Tambahkan
serde(dengan--features derive) danserde_json, lalu#[derive(Serialize, Deserialize)]pada struct-mu. serde_json::to_string/to_string_prettymengubah nilai menjadi JSON;serde_json::from_strmengubah JSON menjadi nilai.from_strmengembalikanResult. Untuk input sungguhan, tangani dengan?ataumatch, bukan.unwrap().- Pakai
Optionatau#[serde(default)]untuk key yang mungkin tidak ada, danrename/rename_allsupaya cocok dengan nama di JSON. - Pakai
serde_json::Valuedanjson!kalau kamu tidak tahu bentuk datanya. Utamakan struct kalau kamu tahu.
Kuis singkat
Section titled “Kuis singkat”Kuis singkat
1. Derive mana yang memungkinkanmu mengubah teks JSON menjadi struct-mu?
2. Sebuah objek JSON mungkin tidak berisi key bio. Tipe apa yang cocok untuk field itu?
3. Struct-mu punya field display_name, tapi JSON-nya memakai displayName. Apa solusi paling sederhana untuk seluruh struct?
4. Secara default, apa yang dilakukan serde dengan key JSON yang tidak ada di struct-mu?
Skor: 0 / 4
Bacaan lebih lanjut
Section titled “Bacaan lebih lanjut”- serde.rs — panduan resmi Serde (bahasa Inggris): gambaran umum cara kerja serde, dengan contoh untuk berbagai format data.
- Container attributes — serde.rs (bahasa Inggris): opsi untuk seluruh struct atau enum, seperti
rename_alldandeny_unknown_fields. - Field attributes — serde.rs (bahasa Inggris): opsi untuk satu field, seperti
rename,default,skip, danskip_serializing_if. serde_json— docs.rs (bahasa Inggris): halaman depan crate ini, dengan contoh JSON bertipe dan tanpa tipe, serta makrojson!.serde_json::Value— docs.rs (bahasa Inggris): semua method untuk membaca JSON tanpa tipe, sepertias_str,as_i64, danget.