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、在非可选字段里冒出一个意外的 null,serde_json::from_str 交还给你的就是一个 Err,而不是你要的数据。所以在 Rust 里,把结构体做对比在大多数语言里都更要紧。

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

如何把 JSON 转成 Rust

把 JSON 转成 Rust 只需三步:

  1. 粘贴你的 JSON。 把一个对象、数组或原始 API 响应丢进输入框。转换即时完成,且完全在客户端运行。
  2. 调整输出。 重命名根结构体、开关 serde derive、加上 Debug 和 Clone,或者去掉 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 要你在 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 转换器。

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