Lewati ke konten

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.

Di dalam proyekmu, tambahkan kedua crate. Feature derive membuat serde menuliskan kode penerjemahnya untukmu:

Terminal window
cargo add serde --features derive
cargo add serde_json

Sekarang 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.

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
}
▶ Coba sendiri

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.

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 tahun
▶ Coba sendiri

Ada dua hal yang perlu diperhatikan:

  • r#"..."# adalah raw string. Di dalamnya, kamu bebas menulis ", sangat praktis untuk JSON.
  • from_str mengembalikan Result, karena teksnya mungkin bukan JSON yang valid. Di sini main mengembalikan Result, jadi kita bisa memakai ? untuk meneruskan error ke atas.

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 15
Error: invalid type: string "old", expected u32 at line 1 column 27
▶ Coba sendiri

Pesan 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 (atau null) menjadi None.
  • Tambahkan #[serde(default)]. Key yang tidak ada akan diisi nilai default dari tipenya: 0, false, String kosong, atau Vec kosong.
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: [] }
▶ Coba sendiri

Pakai Option kalau “tidak ada nilai” punya arti yang berbeda dari “nol” atau “kosong”. Pakai default kalau nilai default-nya sudah cukup.

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"}
▶ Coba sendiri

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.

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 API
2 tugas tersisa
▶ Coba sendiri

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: 18
Teks biasa: Lisbon
Angin: null
▶ Coba sendiri

Perhatikan 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.

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 }
▶ Coba sendiri

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.

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` type

Intinya, 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.

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}]
▶ Coba sendiri

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.

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 }
▶ Coba sendiri

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.

  • Tambahkan serde (dengan --features derive) dan serde_json, lalu #[derive(Serialize, Deserialize)] pada struct-mu.
  • serde_json::to_string / to_string_pretty mengubah nilai menjadi JSON; serde_json::from_str mengubah JSON menjadi nilai.
  • from_str mengembalikan Result. Untuk input sungguhan, tangani dengan ? atau match, bukan .unwrap().
  • Pakai Option atau #[serde(default)] untuk key yang mungkin tidak ada, dan rename / rename_all supaya cocok dengan nama di JSON.
  • Pakai serde_json::Value dan json! kalau kamu tidak tahu bentuk datanya. Utamakan struct kalau kamu tahu.

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