Skip to content
返回博客
教程

JSON 转 Rust 结构体:serde struct 生成完全指南(2026)

正确地把 JSON 转成 Rust 结构体:数字类型推断(i64/u64/f64)、null 用 Option、camelCase 用 serde rename,附常见陷阱。免费在线,数据不离开浏览器。

11 分钟

JSON 转 Rust 结构体:serde struct 生成完全指南(2026)

把你的数据粘贴进 JSON 转 Rust 结构体转换器,复制它生成的 serde struct,JSON 转 Rust 结构体这件事就算做完了:无需安装、无需上传,全程在浏览器里完成。几秒钟就能解决「我现在就要一个结构体」的需求。

不过,生成一个结构体和生成一个正确的结构体是两回事。转换器只能靠猜。而在 Rust 里,猜错的代价更高。在 TypeScript 里猜错顶多退化成一个宽松的 any,你大可无视;但在 serde 里,猜错会在反序列化(deserialize)时直接失败:把浮点数塞进 i64、在非可选字段里冒出一个意外的 nullserde_json::from_str 交还给你的就是一个 Err,而不是你要的数据。所以在 Rust 里,把结构体做对比在大多数语言里都更要紧。

下面讲清楚推断到底是怎么工作的、大多数转换器都做错的数字定型规则、什么时候该用 #[serde(rename)] 而不是 #[serde(rename_all)],以及如何处理日期、动态键和 Rust 关键字,让输出始终能编译通过。

如何把 JSON 转成 Rust

把 JSON 转成 Rust 只需三步:

  1. 粘贴你的 JSON。 把一个对象、数组或原始 API 响应丢进输入框。转换即时完成,且完全在客户端运行。
  2. 调整输出。 重命名根结构体、开关 serde derive、加上 DebugClone,或者去掉 pub 可见性,以贴合你 crate 的风格。
  3. 复制或下载。 一键取走生成的 Rust 代码,直接粘贴进你的项目。

这就是全部的操作路径。如果你的输入是压缩过的,或者你不确定它是否合法,先用 JSON 格式化工具过一遍,让转换器拿到干净、格式规整的 JSON。本指南余下的部分会讲解如何读懂输出,好让你修正那些工具自己推断不出来的情况。

JSON 类型如何映射到 Rust

每一个 JSON 值都有对应的 Rust 类型,但这个映射并非严格一一对应:

JSON 值Rust 类型
"text"String
42i64(大值用 u64,再大用 f64
3.14, 2e3f64
true / falsebool
nullOption<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 要你在 i64u64f64 之间做选择。这个选择本身够写一整节(见下文),因为它正是大多数生成器出错的地方。

转换器如何推断结构体

四条规则几乎能覆盖你喂给它的一切:每种对象形状一个结构体、数组按键逐一合并、精确的数字定型,以及地道的字段命名。

结构推断:每个对象一个具名结构体

每一种不同的对象形状都会独立成为一个具名结构体。嵌套对象不会被内联,而是提升成可引用的独立定义:

{ "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.02e3,无论大小都映射到 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_idi64::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 发来名为 typematch 的键,而这两个都是 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)。原始标识符无法在所有位置表达所有关键字;尤其是 selfcratesuper 会被拒绝作为原始标识符,所以「净化加重命名」是唯一始终能编译通过的做法。像 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 位字段溢出。保留生成出来的 i64u64
  • 一个始终为 null 的字段会变成 Option<serde_json::Value>,而不是普通的可选类型。 工具没有类型可推断,所以把它当作占位符,等你知道确切类型后再给它换上。
  • serde_json::Value 需要 serde_json 依赖。 如果输出里含有 Value,而你的 Cargo.toml 缺了这个 crate,代码就无法编译。
  • 保留为 String 的日期做不了日期运算。 如果你打算比较或格式化时间戳,改用 chrono 类型,而不是自己手动解析字符串。

常见问题

为什么结构体看起来没问题,serde 却拒绝我的 JSON?

几乎总是类型不匹配。两个常见原因是:一个浮点值落进了 i64 字段,以及一个有时会缺失、却没被定型为 Option 的键。serde 会在反序列化时强制校验类型,所以把字段改成 f64Option<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 之类的原始标识符更稳妥,因为原始标识符无法在所有位置覆盖 selfcratesuper

为什么 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 转换器

标签: rust json serde type-safety developer-tools