跳转到内容

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 替你生成翻译代码:

Terminal window
cargo add serde --features derive
cargo 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,所以本页的每个“动手试一试”按钮都不用任何准备就能运行。

给结构体加上 #[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 会加上换行和空格,更适合写进给人看的文件。键的顺序和结构体里字段的顺序一致。

反过来的过程叫反序列化(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 经常和你预想的不一样:少了一个字段,或者数字变成了文本。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 27
▶ 动手试一试

serde 的错误信息是英文的:第二条说“缺少字段 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 返回五十个字段,你也只需要描述自己关心的那几个。

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 的结构,或者只需要其中一小部分。这时就用 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 很灵活,但你会失去编译器的帮助。只要你知道数据的结构,就优先用结构体。

来看一个实际的例子:把应用设置保存到 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()。

写一个结构体 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,看看同一个清单分成多行是什么样子。

把这段 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