Bỏ qua để đến nội dung

JSON với Serde trong Rust

JSON có mặt ở khắp nơi: file cấu hình, API web, dữ liệu lưu game. Trong Rust, gần như ai cũng dùng hai crate để làm việc với nó. serde (viết tắt của serialize / deserialize, tức tuần tự hoá / giải tuần tự hoá) biết cách biến giá trị Rust thành dữ liệu và ngược lại. serde_json dạy nó định dạng JSON.

Hãy hình dung serde như một người phiên dịch. Bạn mô tả dữ liệu một lần, dưới dạng một struct Rust bình thường, và serde dịch nó sang JSON rồi dịch ngược lại giúp bạn. Bạn không bao giờ phải tự viết văn bản JSON, và cũng không phải bóc tách JSON từng ký tự một.

Trong dự án của bạn, hãy thêm cả hai crate. Feature derive cho phép serde tự viết code phiên dịch giúp bạn:

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

Giờ Cargo.toml của bạn có:

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

Nếu Cargo.toml và cargo add còn mới với bạn, hãy đọc Cargo và crate trước. Rust Playground đã có sẵn cả hai crate, nên mọi nút “Try it Yourself” trên trang này đều chạy được mà không cần cài đặt gì.

Thêm #[derive(Serialize)] vào một struct, và serde_json::to_string có thể biến nó thành văn bản 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);
}

Kết quả:

{"name":"Ana","age":28,"admin":false}
{
"name": "Ana",
"age": 28,
"admin": false
}
▶ Tự chạy thử

to_string cho bạn JSON gọn, hợp để gửi qua mạng. to_string_pretty thêm xuống dòng và khoảng trắng, dễ nhìn hơn cho những file mà con người sẽ đọc. Các khoá giữ đúng thứ tự như các trường trong struct của bạn.

Chiều ngược lại gọi là deserialize (giải tuần tự hoá). Thêm Deserialize vào danh sách derive, rồi gọi serde_json::from_str. Bạn cho nó biết bạn muốn kiểu nào bằng một chú thích kiểu:

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!("{} năm nay {} tuổi", user.name, user.age);
Ok(())
}

Kết quả:

User { name: "Ana", age: 28, admin: false }
Ana năm nay 28 tuổi
▶ Tự chạy thử

Có hai điều cần để ý:

  • r#"..."# là một chuỗi thô (raw string). Bên trong nó, bạn viết " thoải mái, rất tiện cho JSON.
  • from_str trả về một Result, vì văn bản có thể không phải JSON hợp lệ. Ở đây main trả về Result, nên ta dùng được ? để đẩy lỗi lên.

JSON thực tế thường không như bạn mong đợi. Thiếu một trường, hoặc một con số lại đến dưới dạng chữ. serde kiểm tra mọi thứ và đưa cho bạn một lỗi rõ ràng thay vì làm sập chương trình. Dùng match để xử lý:

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!("Lỗi: {}", e),
}
}
}

Kết quả:

OK: User { name: "Ana", age: 28 }
Lỗi: missing field `age` at line 1 column 15
Lỗi: invalid type: string "old", expected u32 at line 1 column 27
▶ Tự chạy thử

Thông báo lỗi của serde là tiếng Anh: dòng đầu báo thiếu trường age, dòng sau báo sai kiểu (nhận được chuỗi "old" trong khi cần u32), kèm vị trí dòng và cột. Dữ liệu JSON mẫu được giữ tiếng Anh để vị trí cột trong thông báo không bị thay đổi.

Phần ::<User> được gọi là “turbofish”. Đó là một cách khác để nói bạn muốn kiểu nào, hữu ích khi không có let user: User nào để đặt chú thích kiểu.

Nếu một trường thật sự là tuỳ chọn thì sao? Bạn có hai lựa chọn tốt:

  • Cho trường đó thành Option. Khoá bị thiếu (hoặc null) sẽ thành None.
  • Thêm #[serde(default)]. Khoá bị thiếu sẽ nhận giá trị mặc định của kiểu: 0, false, một String rỗng hay một Vec rỗng.
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": "Mê 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);
}

Kết quả:

Profile { name: "Ana", bio: Some("Mê Rust"), followers: 120, tags: ["rust"] }
Profile { name: "Ben", bio: None, followers: 0, tags: [] }
▶ Tự chạy thử

Dùng Option khi “không có giá trị” mang nghĩa khác với “bằng 0” hay “rỗng”. Dùng default khi giá trị mặc định là đủ tốt.

Rust dùng snake_case cho tên trường, nhưng nhiều API web dùng camelCase, và một số dùng những cái tên hoàn toàn không hợp lệ trong Rust (như type). Bạn không cần phải bỏ những cái tên Rust đẹp đẽ. Dùng rename_all cho cả struct, và rename cho một trường riêng lẻ:

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() {
// kiểu phản hồi mà một API web có thể gửi về
let response = r#"{
"userId": 42,
"displayName": "Ana Silva",
"isVerified": true,
"type": "premium"
}"#;
let profile: UserProfile = serde_json::from_str(response).unwrap();
println!("{} (id {}) có tài khoản {}", profile.display_name, profile.user_id, profile.account_type);
// khi serialize cũng dùng đúng những cái tên đó, nên JSON đi một vòng vẫn y nguyên
println!("{}", serde_json::to_string(&profile).unwrap());
}

Kết quả:

Ana Silva (id 42) có tài khoản premium
{"userId":42,"displayName":"Ana Silva","isVerified":true,"type":"premium"}
▶ Tự chạy thử

Mặc định, serde đơn giản là bỏ qua những khoá JSON mà struct của bạn không có. Vì vậy bạn chỉ cần mô tả những trường mình quan tâm, kể cả khi API gửi về năm mươi trường.

Một mảng JSON trở thành một Vec. Struct cũng có thể chứa struct khác và vector, nên bạn mô tả được JSON lồng sâu chỉ bằng cách lồng các kiểu vào nhau:

use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct Task {
title: String,
done: bool,
}
fn main() {
let text = r#"[
{"title": "Học serde", "done": true},
{"title": "Viết file cấu hình", "done": false},
{"title": "Gọi một 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!("Còn {} việc", left);
}

Kết quả:

[x] Học serde
[ ] Viết file cấu hình
[ ] Gọi một API
Còn 2 việc
▶ Tự chạy thử

Đôi khi bạn không biết trước hình dạng của JSON, hoặc chỉ cần một mẩu nhỏ trong đó. Khi đó hãy dùng serde_json::Value. Nó chứa được mọi loại JSON: object, mảng, chuỗi, số, bool hoặc null. Macro json! dựng một Value bằng cú pháp giống JSON:

use serde_json::{json, Value};
fn main() {
let data: Value = json!({
"city": "Lisbon",
"temps": [18, 21, 19],
"sunny": true
});
println!("Thành phố: {}", data["city"]);
println!("Nhiệt độ đầu tiên: {}", data["temps"][0]);
// as_str() cho bạn một &str thuần thay vì một chuỗi JSON
if let Some(city) = data["city"].as_str() {
println!("Chữ thuần: {}", city);
}
// một khoá không tồn tại cho ra Null thay vì làm sập chương trình
println!("Gió: {}", data["wind"]);
}

Kết quả:

Thành phố: "Lisbon"
Nhiệt độ đầu tiên: 18
Chữ thuần: Lisbon
Gió: null
▶ Tự chạy thử

Hãy để ý rằng in data["city"] có dấu ngoặc kép: nó vẫn là một chuỗi JSON. Dùng .as_str(), .as_i64() hoặc .as_bool() để lấy giá trị Rust thuần. Mỗi phương thức trả về một Option, vì giá trị có thể thuộc kiểu khác.

Value rất linh hoạt, nhưng bạn mất đi sự trợ giúp của trình biên dịch. Hãy ưu tiên struct mỗi khi bạn biết hình dạng dữ liệu.

Đây là một ví dụ thực tế: lưu thiết lập của ứng dụng vào config.json rồi nạp lại. Nó kết hợp trang này với Đọc ghi file. std::fs::write lưu văn bản, còn std::fs::read_to_string nạp nó trở lại:

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,
};
// lưu
let text = serde_json::to_string_pretty(&config)?;
fs::write("config.json", text)?;
// nạp
let loaded_text = fs::read_to_string("config.json")?;
let loaded: Config = serde_json::from_str(&loaded_text)?;
println!("Đã nạp: {:?}", loaded);
Ok(())
}

Kết quả:

Đã nạp: Config { username: "ana", theme: "dark", font_size: 14 }
▶ Tự chạy thử

Sao lại là Box<dyn Error>? Hàm main này có thể thất bại theo hai cách khác nhau: lỗi file (std::io::Error) hoặc lỗi JSON (serde_json::Error). Box<dyn Error> nghĩa là “bất kỳ loại lỗi nào”, nên ? dùng được cho cả hai. Trang Lỗi tự định nghĩa giải thích kỹ hơn về điều này.

Quên derive Deserialize. Nếu struct của bạn chỉ derive Serialize, bạn ghi được JSON nhưng không đọc được. Thông báo lỗi khá dài, nhưng những dòng quan trọng là:

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

Rustc báo User chưa cài đặt serde::Deserialize và gợi ý thêm #[derive(serde::Deserialize)]. Cách sửa: #[derive(Serialize, Deserialize)]. Còn nếu bạn thấy “cannot find derive macro Serialize” (không tìm thấy derive macro Serialize), nghĩa là bạn đã thêm serde mà không có feature derive. Hãy chạy cargo add serde --features derive.

Dùng .unwrap() trên dữ liệu bạn không kiểm soát. Trong ví dụ nhỏ thì không sao, nhưng JSON từ người dùng hay một trang web có thể bị hỏng. Một file sai sẽ làm chương trình sập vì panic. Hãy dùng ? hoặc match như ở trên.

Mong data["name"] in ra không có dấu ngoặc kép. Một Value được in dưới dạng JSON. Gọi .as_str() để lấy chữ thuần.

Hãy tạo struct Item có name (String) và qty (u32). Đặt hai món vào một Vec, biến cả vector thành JSON chỉ bằng một lời gọi, rồi in ra.

Gợi ý: chỉ struct cần #[derive(Serialize)]. Một Vec chứa những thứ serialize được thì cũng serialize được, và nó trở thành một mảng JSON.

Xem lời giải
use serde::Serialize;
#[derive(Serialize)]
struct Item {
name: String,
qty: u32,
}
fn main() {
let list = vec![
Item { name: String::from("táo"), qty: 3 },
Item { name: String::from("bánh mì"), qty: 1 },
];
let json = serde_json::to_string(&list).unwrap();
println!("{}", json);
}

Kết quả:

[{"name":"táo","qty":3},{"name":"bánh mì","qty":1}]
▶ Tự chạy thử

serde vốn đã biết cách xử lý Vec, String và số, nên chỉ cần derive Serialize cho Item là đủ. Hãy thử to_string_pretty để thấy cùng danh sách đó trải ra trên nhiều dòng.

Hãy đọc JSON này vào một struct Settings: {"siteTitle": "Blog của tôi", "postsPerPage": 5}. Struct phải có site_title, posts_per_page, và một theme có thể bị thiếu. Sau đó đọc tiếp {"siteTitle": "Ghi chú"}, trong đó posts_per_page phải lùi về 0. In cả hai bằng {:?}.

Gợi ý: bạn cần #[serde(rename_all = "camelCase")] trên struct, Option<String> cho theme, và #[serde(default)] trên posts_per_page.

Xem lời giải
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 của tôi", "postsPerPage": 5}"#;
let short = r#"{"siteTitle": "Ghi chú"}"#;
let a: Settings = serde_json::from_str(full).unwrap();
let b: Settings = serde_json::from_str(short).unwrap();
println!("{:?}", a);
println!("{:?}", b);
}

Kết quả:

Settings { site_title: "Blog của tôi", posts_per_page: 5, theme: None }
Settings { site_title: "Ghi chú", posts_per_page: 0, theme: None }
▶ Tự chạy thử

rename_all ánh xạ siteTitle sang site_title và postsPerPage sang posts_per_page. Cả hai JSON đều không có theme, nên cả hai lần nó đều là None, còn postsPerPage bị thiếu ở JSON thứ hai trở thành 0. Bỏ #[serde(default)] đi và unwrap() thứ hai sẽ panic với “missing field postsPerPage” (thiếu trường postsPerPage). Ở đây .unwrap() không sao vì chính ta viết JSON; với dữ liệu thật, hãy dùng ? hoặc match.

  • Thêm serde (với --features derive) và serde_json, rồi đặt #[derive(Serialize, Deserialize)] lên các struct.
  • serde_json::to_string / to_string_pretty biến một giá trị thành JSON; serde_json::from_str biến JSON thành một giá trị.
  • from_str trả về một Result. Với dữ liệu thật, hãy xử lý bằng ? hoặc match, không phải .unwrap().
  • Dùng Option hoặc #[serde(default)] cho những khoá có thể bị thiếu, và rename / rename_all để khớp với tên trong JSON.
  • Dùng serde_json::Value và json! khi bạn không biết hình dạng dữ liệu. Khi biết thì hãy ưu tiên struct.

Kiểm tra nhanh

1. Derive nào cho phép bạn biến văn bản JSON thành struct của mình?

2. Một object JSON có thể không chứa khoá bio. Kiểu nào phù hợp cho trường đó?

3. Struct của bạn có trường display_name, nhưng JSON dùng displayName. Cách sửa đơn giản nhất cho cả struct là gì?

4. Mặc định serde làm gì với những khoá JSON mà struct của bạn không có?

Điểm: 0 / 4