JSON 转 Rust 结构体:serde struct 生成完全指南(2026)
把你的数据粘贴进 JSON 转 Rust 结构体转换器,复制它生成的 serde struct,JSON 转 Rust 结构体这件事就算做完了:无需安装、无需上传,全程在浏览器里完成。几秒钟就能解决「我现在就要一个结构体」的需求。
不过,生成一个结构体和生成一个正确的结构体是两回事。转换器只能靠猜。而在 Rust 里,猜错的代价更高。在 TypeScript 里猜错顶多退化成一个宽松的 any,你大可无视;但在 serde 里,猜错会在反序列化(deserialize)时直接失败:把浮点数塞进 i64、在非可选字段里冒出一个意外的 null,serde_json::from_str 交还给你的就是一个 Err,而不是你要的数据。所以在 Rust 里,把结构体做对比在大多数语言里都更要紧。
下面讲清楚推断到底是怎么工作的、大多数转换器都做错的数字定型规则、什么时候该用 #[serde(rename)] 而不是 #[serde(rename_all)],以及如何处理日期、动态键和 Rust 关键字,让输出始终能编译通过。
如何把 JSON 转成 Rust
把 JSON 转成 Rust 只需三步:
- 粘贴你的 JSON。 把一个对象、数组或原始 API 响应丢进输入框。转换即时完成,且完全在客户端运行。
- 调整输出。 重命名根结构体、开关 serde derive、加上
Debug和Clone,或者去掉pub可见性,以贴合你 crate 的风格。 - 复制或下载。 一键取走生成的 Rust 代码,直接粘贴进你的项目。
这就是全部的操作路径。如果你的输入是压缩过的,或者你不确定它是否合法,先用 JSON 格式化工具过一遍,让转换器拿到干净、格式规整的 JSON。本指南余下的部分会讲解如何读懂输出,好让你修正那些工具自己推断不出来的情况。
JSON 类型如何映射到 Rust
每一个 JSON 值都有对应的 Rust 类型,但这个映射并非严格一一对应:
| JSON 值 | Rust 类型 |
|---|---|
"text" | String |
42 | i64(大值用 u64,再大用 f64) |
3.14, 2e3 | f64 |
true / false | bool |
null | Option<T>(或 Option<serde_json::Value>) |
[1, 2, 3] | Vec<T> |
{ ... } | 具名 struct |
对象才是真正需要动脑的地方。看一个典型的 REST 数据:
{
"id": 101,
"name": "Ada Lovelace",
"email": "ada@example.com",
"active": true,
"roles": ["admin", "user"]
}
转换器会产出一个可直接用于 serde 的结构体:
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
pub id: i64,
pub name: String,
pub email: String,
pub active: bool,
pub roles: Vec<String>,
}
每个标量都映射到一个原生类型,roles 变成 Vec<String>。唯一不能干净映射的 JSON 类型是 number:JSON 只有一种数字类型,而 Rust 要你在 i64、u64 和 f64 之间做选择。这个选择本身够写一整节(见下文),因为它正是大多数生成器出错的地方。
转换器如何推断结构体
四条规则几乎能覆盖你喂给它的一切:每种对象形状一个结构体、数组按键逐一合并、精确的数字定型,以及地道的字段命名。
结构推断:每个对象一个具名结构体
每一种不同的对象形状都会独立成为一个具名结构体。嵌套对象不会被内联,而是提升成可引用的独立定义:
{ "repo": "serde", "owner": { "login": "dtolnay", "id": 100 } }
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
pub repo: String,
pub owner: Owner,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Owner {
pub login: String,
pub id: i64,
}
嵌套的 owner 独立成一个 Owner 结构体,由字段引用。相同的形状会被去重,因此两个结构相同的字段共用一个结构体,而不是各自复制一份。
数组合并与 Option 字段
当你传入一个对象数组时,转换器会按键逐一合并它们。只出现在部分元素、而非全部元素里的键会变成 Option:
{ "users": [{ "id": 1, "nick": "x" }, { "id": 2 }] }
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
pub users: Vec<User>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
pub id: i64,
pub nick: Option<String>,
}
id 出现在每个元素里,所以它保持必填。nick 在第二个用户里缺失,所以它变成 Option<String>。注意这里没有什么:没有 #[serde(default)]。serde 本就把 Option 当作可选,会把缺失的键反序列化为 None,所以这个属性是多余的。
正确的数字定型:i64、u64、f64
这条规则是竞品工具略过的,也正是在真实数据上出问题的那条。生成器采用三档策略。整数映射到 i64。如果某个值超过 i64::MAX,就提升为 u64。超过 u64::MAX,则回退到 f64。任何带小数点或指数写法的 token,比如 1.0 或 2e3,无论大小都映射到 f64。
{ "user_id": 12000000000000000000, "balance": 19.99, "retries": 3 }
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
pub user_id: u64,
pub balance: f64,
pub retries: i64,
}
这里 user_id 比 i64::MAX 大,所以它落到 u64,依然能来回无损转换。balance 带小数点,所以它是 f64,不是整数。这一点很重要,因为 serde 会在反序列化时强制校验类型。如果你给它一个把雪花(snowflake)ID 或 Twitter 风格 ID 定型为 i32 的结构体,值就会溢出;把一个浮点字段定型为 i64,serde 会返回错误,而不是悄悄截断。正是这套三档规则,让生成出来的 rust struct from json 能顺利反序列化,而不是在遇到第一个大 ID 时就 panic。
snake_case 字段与 #[serde(rename)]
JSON 的键经常是 camelCase;地道的 Rust 字段是 snake_case。转换器会重命名字段,并加上一个 #[serde(rename)],把它映射回原本那个 JSON 键:
{ "login": "octocat", "publicRepos": 15, "followerCount": 9001, "createdAt": "2011-01-25" }
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
pub login: String,
#[serde(rename = "publicRepos")]
pub public_repos: i64,
#[serde(rename = "followerCount")]
pub follower_count: i64,
#[serde(rename = "createdAt")]
pub created_at: String,
}
像 login 这种本就是 snake_case 的键不会被重命名。其余的键都能干净地映射回线上格式(wire format),同时你的 Rust 代码读起来也自然。
#[serde(rename)] 对比 #[serde(rename_all)]
工具默认对每个字段单独发出 #[serde(rename)],因为它永远有效——哪怕同一个对象里混着 camelCase、snake_case 和不规则的键。它无需去揣测一个可能根本不存在的统一命名约定。
当一个结构体里的每个字段确实共享同一种约定时,你可以把这些属性收拢成一个容器级别的重命名。删掉逐字段的那些行,在结构体上放一个 #[serde(rename_all = "camelCase")]:
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Root {
pub login: String,
pub public_repos: i64,
pub follower_count: i64,
pub created_at: String,
}
两种写法反序列化的是同一份 JSON。键混杂或不规则时用逐字段的 rename;结构体统一、你又想少些干扰时就用 rename_all。同一个属性上,serde 还提供了其他约定,比如 "snake_case"、"kebab-case" 和 "SCREAMING_SNAKE_CASE"。
Rust 关键字与非标识符键
JSON 的键只是字符串,所以没什么能阻止某个 API 发来名为 type 或 match 的键,而这两个都是 Rust 的保留字。生成器会把它们净化成一个合法标识符,再加一个重命名:
{ "type": "user", "match": true, "first-name": "Ada" }
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
#[serde(rename = "type")]
pub type_: String,
#[serde(rename = "match")]
pub match_: bool,
#[serde(rename = "first-name")]
pub first_name: String,
}
这里特意选择尾部加下划线的形式(type_),而不是像 r#type 那样的原始标识符(raw identifier)。原始标识符无法在所有位置表达所有关键字;尤其是 self、crate 和 super 会被拒绝作为原始标识符,所以「净化加重命名」是唯一始终能编译通过的做法。像 first-name,或以数字开头的 2fa 这种非标识符键,也享受同样的待遇:一个合法的 Rust 名字,再加一个映射回字面 JSON 键的重命名。
用 serde_json 反序列化
拿到结构体后,把依赖加进 Cargo.toml:
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
然后一行代码就能解析。下面这个示例是完整的,可以原样编译:
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
pub id: i64,
pub name: String,
pub email: String,
pub active: bool,
pub roles: Vec<String>,
}
fn main() -> Result<(), serde_json::Error> {
let data = r#"{"id":101,"name":"Ada Lovelace","email":"a@ex.com","active":true,"roles":["admin"]}"#;
let root: Root = serde_json::from_str(data)?;
println!("{} has {} role(s)", root.name, root.roles.len());
Ok(())
}
Deserialize derive 包办了全部工作。字符串用 serde_json::from_str,字节切片用 from_slice,文件或 HTTP body 用 from_reader,而 serde_json::to_string 则把值序列化回 JSON。这就是把类型做对的回报:你生成的 serde derive 是一个运行时校验器,因此一次返回 Ok 的 serde_json 反序列化调用,是数据与你的结构体相符的保证,而不仅仅是编译器「以为」它相符。
日期、动态键与未知字段
有些形状无法映射成普通的结构体字段,生成器会给它们一个合理的默认值,之后由你手动收紧。
日期。 JSON 没有日期类型,所以一个 ISO 或 RFC 3339 时间戳会以普通 String 的形式抵达。要真正处理日期,把该字段换成 chrono 类型,并启用 chrono 的 serde feature(chrono = { version = "0.4", features = ["serde"] })。之后 serde 就会自动解析 RFC 3339:
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Event {
pub name: String,
pub created_at: DateTime<Utc>,
}
动态键。 当键在运行时变化时,比如一个从 ID 到值的映射,固定结构体就是错误的形状。用一个以 String 为键的 HashMap:
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
pub scores: HashMap<String, i64>,
}
未知的额外字段。 想保留一个带类型的结构体,同时又捕获你没有建模的键,可以加一个由 HashMap 支撑的 #[serde(flatten)] 字段。对于真正动态的值,serde_json::Value 是万能兜底。当你真正的需求是校验结构而非生成类型时,JSON Schema 校验指南讲解了如何端到端地落实一份契约。
json-to-rust、quicktype、typeshare 与手写的对比
从 JSON 生成 Rust 类型没有唯一的最佳方式。它取决于 JSON 存在何处,以及你在往哪个方向转换。
| 方案 | 适用场景 | 说明 |
|---|---|---|
| 在线转换器(本工具、transform.tools) | 一次性转换、敏感数据、零安装 | 从样本推断,完全在客户端 |
| quicktype | 多语言输出、流水线代码生成 | 同样由样本驱动;命令行或网页 |
| typeshare | 把已有的 Rust 类型共享给 TS、Swift、Kotlin | 相反方向:Rust 转其他语言 |
| schemafy / typify | 从 JSON Schema 生成 | 输入是 schema,不是样本 |
| 手写结构体 | 极小的数据、学习 serde | 完全掌控,但规模一大就繁琐易错 |
核心区别在于:在线转换器和 quicktype 都从一份 JSON样本推断类型,而 typeshare 反着来,把带注解的 Rust 类型转成 TypeScript 或 Swift。schemafy 和 typify 接受的是 JSON Schema,而非示例数据。如果你的代码库是 TypeScript 而不是 Rust,同样的样本驱动思路可以配合 JSON 转 TypeScript 转换器使用,而 JSON 转 TypeScript 接口指南则深入讲解了那一侧。
从 JSON 生成 Rust 时的常见陷阱
生成的结构体只是起点。在把输出用于线上真实数据之前,留意下面这些。
- 单个样本很少能揭示每一种形状。 可选字段只能从多个数组元素里推断出来。粘贴一个有代表性的数组,让
Option的推断准确,而不是从一个碰巧的对象里瞎猜。 - 空数组或混合类型数组会回退到
serde_json::Value。 在无从推断时,这是最诚实的答案。喂一个更丰富的样本,才能得到具体的元素类型。 - 不要把一个大 ID 收窄成
i32。 雪花 ID 及类似 ID 超过 2^53,会让一个 32 位字段溢出。保留生成出来的i64或u64。 - 一个始终为
null的字段会变成Option<serde_json::Value>,而不是普通的可选类型。 工具没有类型可推断,所以把它当作占位符,等你知道确切类型后再给它换上。 serde_json::Value需要serde_json依赖。 如果输出里含有Value,而你的Cargo.toml缺了这个 crate,代码就无法编译。- 保留为
String的日期做不了日期运算。 如果你打算比较或格式化时间戳,改用 chrono 类型,而不是自己手动解析字符串。
常见问题
为什么结构体看起来没问题,serde 却拒绝我的 JSON?
几乎总是类型不匹配。两个常见原因是:一个浮点值落进了 i64 字段,以及一个有时会缺失、却没被定型为 Option 的键。serde 会在反序列化时强制校验类型,所以把字段改成 f64 或 Option<T>,或者从一个有代表性的样本重新生成结构体。
JSON 整数应该变成哪种 Rust 数字类型?
默认用 i64。当值超过 i64::MAX 时提升为 u64,超过 u64::MAX 则回退到 f64。任何带小数点或指数写法的数字,无论大小都映射到 f64,因为 serde 会拒绝把浮点数反序列化进整数字段。
当 JSON 省略某字段时,我该如何让它变成可选?
把它定型为 Option<T>。serde 会自动把 Option 当作可选,并把缺失的键反序列化为 None,所以你不需要 #[serde(default)]。当部分样本条目缺少某个键时,转换器就会把该字段标记为 Option。
我该用 #[serde(rename)] 还是 #[serde(rename_all)]?
当数据混用多种命名风格时,用逐字段的 #[serde(rename)];它永远有效。如果一个结构体里每个字段都遵循同一种约定,就删掉逐字段的属性,改在结构体上放一个 #[serde(rename_all = "camelCase")]。两者反序列化的结果完全一样。
遇到像 type 这样是 Rust 关键字的 JSON 键,我该怎么处理?
生成器会产出 type_,并配上一个 #[serde(rename = "type")] 映射回原始键。这比 r#type 之类的原始标识符更稳妥,因为原始标识符无法在所有位置覆盖 self、crate 和 super。
为什么 JSON 日期被定型为 String,而不是日期类型?
JSON 没有日期类型,所以时间戳在传输时就只是个字符串,String 是最诚实的默认选择。要真正处理日期,把字段改成 chrono 的 DateTime<Utc> 或 NaiveDate,并启用 chrono 的 serde feature;之后 serde 就会替你解析 RFC 3339。
我该如何把 JSON 反序列化进生成的结构体?
把 serde_json 加进 Cargo.toml,然后写 let root: Root = serde_json::from_str(json)?;。剩下的交给 Deserialize derive。字节切片用 from_slice,文件或 HTTP body 用 from_reader。
使用在线 JSON 转 Rust 结构体转换器时,我的 JSON 是私密的吗?
是的。转换 100% 通过 JavaScript 在你的浏览器里运行。你的 JSON,包括令牌、ID 和客户数据,绝不会离开页面,也绝不会被发往任何服务器。准备好之后,用你自己的数据试试 JSON 转 Rust 转换器。