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.
Cài đặt
Phần tiêu đề “Cài đặ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:
cargo add serde --features derivecargo add serde_jsonGiờ 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ì.
Từ struct sang JSON
Phần tiêu đề “Từ struct sang JSON”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}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.
Từ JSON sang struct
Phần tiêu đề “Từ JSON sang struct”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ổiCó 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_strtrả về mộtResult, vì văn bản có thể không phải JSON hợp lệ. Ở đâymaintrả vềResult, nên ta dùng được?để đẩy lỗi lên.
Khi JSON bị sai
Phần tiêu đề “Khi JSON bị sai”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 15Lỗi: invalid type: string "old", expected u32 at line 1 column 27Thô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.
Khoá bị thiếu: Option và #[serde(default)]
Phần tiêu đề “Khoá bị thiếu: Option và #[serde(default)]”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ặcnull) sẽ thànhNone. - Thêm
#[serde(default)]. Khoá bị thiếu sẽ nhận giá trị mặc định của kiểu:0,false, mộtStringrỗng hay mộtVecrỗ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: [] }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.
Đổi tên trường
Phần tiêu đề “Đổi tên trường”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"}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.
Danh sách: Vec chứa struct
Phần tiêu đề “Danh sách: Vec chứa struct”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 APICòn 2 việcJSON không định kiểu: Value và json!
Phần tiêu đề “JSON không định kiểu: Value và json!”Đô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: 18Chữ thuần: LisbonGió: nullHã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.
Đọc và ghi file JSON
Phần tiêu đề “Đọc và ghi file JSON”Đâ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 }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.
Lỗi thường gặp
Phần tiêu đề “Lỗi thường gặp”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` typeRustc 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.
Luyện tập
Phần tiêu đề “Luyện tập”1. Lưu danh sách đi chợ
Phần tiêu đề “1. Lưu danh sách đi chợ”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}]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.
2. Thiết lập có giá trị mặc định
Phần tiêu đề “2. Thiết lập có giá trị mặc định”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 }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.
Tóm tắt
Phần tiêu đề “Tóm tắt”- 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_prettybiến một giá trị thành JSON;serde_json::from_strbiến JSON thành một giá trị.from_strtrả về mộtResult. Với dữ liệu thật, hãy xử lý bằng?hoặcmatch, không phải.unwrap().- Dùng
Optionhoặ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::Valuevà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
Phần tiêu đề “Kiểm tra nhanh”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
Đọc thêm
Phần tiêu đề “Đọc thêm”- serde.rs — the official Serde guide (tiếng Anh): tổng quan cách serde hoạt động, kèm ví dụ cho nhiều định dạng dữ liệu.
- Container attributes — serde.rs (tiếng Anh): các tuỳ chọn cho cả một struct hoặc enum, như
rename_allvàdeny_unknown_fields. - Field attributes — serde.rs (tiếng Anh): các tuỳ chọn cho từng trường, như
rename,default,skipvàskip_serializing_if. serde_json— docs.rs (tiếng Anh): trang chính của crate, với ví dụ về JSON có kiểu, JSON không định kiểu và macrojson!.serde_json::Value— docs.rs (tiếng Anh): mọi phương thức để đọc JSON không định kiểu, nhưas_str,as_i64vàget.