Rust 用 Serde 处理 JSON
JSON 无处不在:配置文件、Web API、游戏存档。在 Rust 里,几乎所有人都用两个 crate 来处理它。serde(取自 serialize / deserialize,即序列化 / 反序列化)知道怎样把 Rust 的值变成数据,再变回来。serde_json 则教会它 JSON 这种格式。
可以把 serde 想象成一位翻译。你只要把数据描述一次,写成普通的 Rust 结构体,serde 就会替你把它翻译成 JSON,也能再翻译回来。你永远不用手写 JSON 文本,也不用一个字符一个字符地去拆解 JSON。
在你的项目里添加这两个 crate。derive feature 让 serde 替你生成翻译代码:
cargo add serde --features derivecargo add serde_json现在你的 Cargo.toml 里有了:
[dependencies]serde = { version = "1.0", features = ["derive"] }serde_json = "1.0"如果你还不熟悉 Cargo.toml 和 cargo add,先读一读 Cargo 与 crate。Rust Playground 已经内置了这两个 crate,所以本页的每个“动手试一试”按钮都不用任何准备就能运行。
把结构体转成 JSON
Section titled “把结构体转成 JSON”给结构体加上 #[derive(Serialize)],serde_json::to_string 就能把它变成 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);}输出:
{"name":"Ana","age":28,"admin":false}{ "name": "Ana", "age": 28, "admin": false}to_string 生成紧凑的 JSON,适合通过网络发送。to_string_pretty 会加上换行和空格,更适合写进给人看的文件。键的顺序和结构体里字段的顺序一致。
把 JSON 转成结构体
Section titled “把 JSON 转成结构体”反过来的过程叫反序列化(deserializing)。在 derive 列表里加上 Deserialize,然后调用 serde_json::from_str。用类型注解告诉它你想要什么类型:
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!("{} 今年 {} 岁", user.name, user.age); Ok(())}输出:
User { name: "Ana", age: 28, admin: false }Ana 今年 28 岁有两点值得注意:
r#"..."#是原始字符串(raw string)。在它里面可以随便写",写 JSON 时特别方便。from_str返回Result,因为文本可能不是合法的 JSON。这里main返回Result,所以可以用?把错误往上传。
JSON 不对的时候
Section titled “JSON 不对的时候”现实中的 JSON 经常和你预想的不一样:少了一个字段,或者数字变成了文本。serde 会检查所有内容,出错时给你一个清楚的错误,而不是直接崩溃。用 match 来处理:
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!("成功:{:?}", user), Err(e) => println!("错误:{}", e), } }}输出:
成功:User { name: "Ana", age: 28 }错误:missing field `age` at line 1 column 15错误:invalid type: string "old", expected u32 at line 1 column 27serde 的错误信息是英文的:第二条说“缺少字段 age,位于第 1 行第 15 列”,第三条说“类型无效:得到的是字符串 "old",期望的是 u32”。
::<User> 这部分叫作 “turbofish”。它是指定类型的另一种写法,在没有 let user: User 可以写类型注解的地方很好用。
缺少的键:Option 和 #[serde(default)]
Section titled “缺少的键:Option 和 #[serde(default)]”如果某个字段确实是可选的呢?你有两个不错的选择:
- 把字段写成
Option。键不存在(或者是null)时,它就是None。 - 加上
#[serde(default)]。键不存在时,字段取该类型的默认值:0、false、空String或空Vec。
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": "热爱 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);}输出:
Profile { name: "Ana", bio: Some("热爱 Rust"), followers: 120, tags: ["rust"] }Profile { name: "Ben", bio: None, followers: 0, tags: [] }当“没有值”和“零”或“空”意思不同时,用 Option。当默认值就够用时,用 default。
Rust 的字段名用 snake_case,但很多 Web API 用 camelCase,有些甚至用在 Rust 里根本不合法的名字(比如 type)。你不必为此放弃好看的 Rust 名字。整个结构体用 rename_all,单个字段用 rename:
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() { // 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 {})的账户类型是 {}", profile.display_name, profile.user_id, profile.account_type);
// 序列化时用的是同样的名字,所以 JSON 可以原样往返 println!("{}", serde_json::to_string(&profile).unwrap());}输出:
Ana Silva(id 42)的账户类型是 premium{"userId":42,"displayName":"Ana Silva","isVerified":true,"type":"premium"}默认情况下,serde 会直接忽略结构体里没有的 JSON 键。所以即使 API 返回五十个字段,你也只需要描述自己关心的那几个。
一组数据:结构体的 Vec
Section titled “一组数据:结构体的 Vec”JSON 数组会变成 Vec。结构体里还可以包含别的结构体和 vector,所以只要把类型嵌套起来,就能描述层次很深的 JSON:
use serde::Deserialize;
#[derive(Deserialize, Debug)]struct Task { title: String, done: bool,}
fn main() { let text = r#"[ {"title": "学习 serde", "done": true}, {"title": "写一个配置文件", "done": false}, {"title": "调用一个 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!("还剩 {} 个任务", left);}输出:
[x] 学习 serde[ ] 写一个配置文件[ ] 调用一个 API还剩 2 个任务无类型的 JSON:Value 和 json!
Section titled “无类型的 JSON:Value 和 json!”有时候你事先不知道 JSON 的结构,或者只需要其中一小部分。这时就用 serde_json::Value。它能装下任何 JSON:对象、数组、字符串、数字、布尔值或 null。json! 宏可以用类似 JSON 的语法构建一个 Value:
use serde_json::{json, Value};
fn main() { let data: Value = json!({ "city": "里斯本", "temps": [18, 21, 19], "sunny": true });
println!("城市:{}", data["city"]); println!("第一个温度:{}", data["temps"][0]);
// as_str() 给你一个普通的 &str,而不是 JSON 字符串 if let Some(city) = data["city"].as_str() { println!("纯文本:{}", city); }
// 不存在的键会得到 Null,而不是崩溃 println!("风力:{}", data["wind"]);}输出:
城市:"里斯本"第一个温度:18纯文本:里斯本风力:null注意,打印 data["city"] 时带着引号:它仍然是一个 JSON 字符串。用 .as_str()、.as_i64() 或 .as_bool() 可以拿到普通的 Rust 值。它们都返回 Option,因为这个值可能是别的类型。
Value 很灵活,但你会失去编译器的帮助。只要你知道数据的结构,就优先用结构体。
读写 JSON 文件
Section titled “读写 JSON 文件”来看一个实际的例子:把应用设置保存到 config.json,再重新读出来。它把本页和文件读写结合在一起。std::fs::write 负责保存文本,std::fs::read_to_string 负责读回来:
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, };
// 保存 let text = serde_json::to_string_pretty(&config)?; fs::write("config.json", text)?;
// 读取 let loaded_text = fs::read_to_string("config.json")?; let loaded: Config = serde_json::from_str(&loaded_text)?; println!("已读取:{:?}", loaded); Ok(())}输出:
已读取:Config { username: "ana", theme: "dark", font_size: 14 }为什么用 Box<dyn Error>?这个 main 可能以两种不同的方式失败:文件错误(std::io::Error)或 JSON 错误(serde_json::Error)。Box<dyn Error> 的意思是“任何一种错误”,所以 ? 对两者都适用。自定义错误那一页有更详细的讲解。
忘了派生 Deserialize。 如果结构体只派生了 Serialize,你就只能写出 JSON,却读不进来。错误信息很长,但关键的是这几行:
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意思是 User 不满足 trait 约束 serde::Deserialize<'de>,编译器还建议你给 User 加上 #[derive(serde::Deserialize)]。修法:写成 #[derive(Serialize, Deserialize)]。如果你看到的是 “cannot find derive macro Serialize”(找不到 derive 宏 Serialize),说明添加 serde 时没有带上 derive feature。运行 cargo add serde --features derive 即可。
对不受你控制的数据使用 .unwrap()。 在小例子里这样写没问题,但来自用户或网站的 JSON 可能是坏的。一个坏文件就会让你的程序 panic 崩溃。像上面那样用 ? 或 match。
以为 data["name"] 打印出来不带引号。 Value 打印出来就是 JSON。想要纯文本,调用 .as_str()。
1. 保存购物清单
Section titled “1. 保存购物清单”写一个结构体 Item,包含 name(String)和 qty(u32)。把两个物品放进一个 Vec,用一次调用把整个 vector 转成 JSON,然后打印出来。
提示:只有结构体需要 #[derive(Serialize)]。由可序列化元素组成的 Vec 本身也可以序列化,它会变成一个 JSON 数组。
查看答案
use serde::Serialize;
#[derive(Serialize)]struct Item { name: String, qty: u32,}
fn main() { let list = vec![ Item { name: String::from("苹果"), qty: 3 }, Item { name: String::from("面包"), qty: 1 }, ];
let json = serde_json::to_string(&list).unwrap(); println!("{}", json);}输出:
[{"name":"苹果","qty":3},{"name":"面包","qty":1}]serde 本来就知道怎样处理 Vec、String 和数字,所以只要给 Item 派生 Serialize 就够了。试试 to_string_pretty,看看同一个清单分成多行是什么样子。
2. 带默认值的设置
Section titled “2. 带默认值的设置”把这段 JSON 读进一个 Settings 结构体:{"siteTitle": "我的博客", "postsPerPage": 5}。结构体应该有 site_title、posts_per_page,以及一个可能不存在的 theme。然后再读取 {"siteTitle": "笔记"},这时 posts_per_page 应该回退为 0。用 {:?} 把两个都打印出来。
提示:结构体上需要 #[serde(rename_all = "camelCase")],theme 用 Option<String>,posts_per_page 上加 #[serde(default)]。
查看答案
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": "我的博客", "postsPerPage": 5}"#; let short = r#"{"siteTitle": "笔记"}"#;
let a: Settings = serde_json::from_str(full).unwrap(); let b: Settings = serde_json::from_str(short).unwrap();
println!("{:?}", a); println!("{:?}", b);}输出:
Settings { site_title: "我的博客", posts_per_page: 5, theme: None }Settings { site_title: "笔记", posts_per_page: 0, theme: None }rename_all 把 siteTitle 对应到 site_title,把 postsPerPage 对应到 posts_per_page。两段 JSON 都没有 theme,所以两次都是 None;第二段缺少 postsPerPage,于是变成 0。去掉 #[serde(default)],第二个 unwrap() 就会 panic,报 “missing field postsPerPage”(缺少字段 postsPerPage)。这里用 .unwrap() 没问题,因为 JSON 是我们自己写的;对于真实的输入,请用 ? 或 match。
- 添加
serde(带上--features derive)和serde_json,然后给结构体加上#[derive(Serialize, Deserialize)]。 serde_json::to_string/to_string_pretty把值转成 JSON;serde_json::from_str把 JSON 转成值。from_str返回Result。处理真实输入时,用?或match,而不是.unwrap()。- 可能缺少的键用
Option或#[serde(default)];用rename/rename_all对上 JSON 里的名字。 - 不知道数据结构时,用
serde_json::Value和json!;知道的话,优先用结构体。
小测验
1. 哪个 derive 能让你把 JSON 文本转成自己的结构体?
2. 一个 JSON 对象里可能没有 bio 这个键。这个字段用什么类型比较好?
3. 你的结构体有个字段叫 display_name,但 JSON 里用的是 displayName。对整个结构体来说,最简单的修法是什么?
4. 对于结构体里没有的 JSON 键,serde 默认会怎么做?
得分: 0 / 4
- serde.rs — Serde 官方指南(英文):serde 的工作原理概览,附多种数据格式的示例。
- Container attributes — serde.rs(英文):作用于整个结构体或枚举的选项,比如
rename_all和deny_unknown_fields。 - Field attributes — serde.rs(英文):作用于单个字段的选项,比如
rename、default、skip和skip_serializing_if。 serde_json— docs.rs(英文):这个 crate 的首页,有带类型和无类型 JSON 的示例,以及json!宏的用法。serde_json::Value— docs.rs(英文):读取无类型 JSON 的所有方法,比如as_str、as_i64和get。