跳转到正文
zeno's blog
返回

Rust 基础:Serde 序列化框架

专题: Rust 基础

Table of contents

Open Table of contents

架构:抽象与实现分离

serde(框架层)
  定义两个 trait: Serialize, Deserialize
  定义中间数据模型: bool, i64, string, seq, map, struct, enum...
  提供 #[derive] 宏 + #[serde(...)] 属性

         ↓ 连接

格式 crate(实现层)— 各自独立,互不依赖
  serde_json   → JSON
  bincode      → 紧凑二进制
  toml         → TOML 配置文件
  serde_yaml   → YAML
  ciborium     → CBOR(二进制 JSON)
  postcard     → 嵌入式友好的二进制
  rmp-serde    → MessagePack

你写一次 #[derive(Serialize, Deserialize)],所有格式 crate 都能用:

#[derive(Serialize, Deserialize)]
struct Player { name: String, level: u32 }

serde_json::to_string(&player)?;     // {"name":"Bear","level":15}
bincode::serialize(&player)?;        // [4, 66, 101, 97, 114, 15, 0, 0, 0]
toml::to_string(&player)?;           // name = "Bear"\nlevel = 15

Cargo.toml

[dependencies]
serde = { version = "1", features = ["derive"] }  # 框架 + derive 宏
serde_json = "1"      # JSON 格式
bincode = "1"         # 二进制格式
toml = "0.8"          # TOML 格式

features = ["derive"] 启用 #[derive(Serialize, Deserialize)],几乎必开。


serde 核心:Serialize 和 Deserialize trait

// 简化概念
trait Serialize {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error>;
    // "把自己描述给一个 serializer"
    // serializer 是 JSON/bincode/TOML 等具体实现
}

trait Deserialize<'de> {
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error>;
    // "从一个 deserializer 中构造自己"
}

你不需要手写这些。#[derive] 自动生成。


#[derive] 基本用法

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
struct Player {
    name: String,
    level: u32,
    health: f32,
    items: Vec<String>,
    position: Option<[f32; 3]>,
}

// 序列化
let player = Player {
    name: "Bear".into(),
    level: 15,
    health: 87.5,
    items: vec!["AK-47".into(), "Bandage".into()],
    position: Some([10.0, 0.0, 5.0]),
};
let json = serde_json::to_string(&player)?;
// {"name":"Bear","level":15,"health":87.5,"items":["AK-47","Bandage"],"position":[10.0,0.0,5.0]}

// 反序列化
let player: Player = serde_json::from_str(&json)?;

支持的类型

基本类型:   bool, i8~i128, u8~u128, f32, f64, char, String, &str
容器:      Vec<T>, HashMap<K,V>, HashSet<T>, BTreeMap, VecDeque
Option:    Option<T> → JSON null / 值
元组:      (A, B, C) → JSON 数组
数组:      [T; N] → JSON 数组
枚举:      见下文
嵌套结构体: 递归序列化
Box/Arc/Rc: 透明序列化内部值

#[serde(…)] 属性详解

容器属性(加在 struct/enum 上)

// 字段命名风格转换
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]  // Rust snake_case → JSON camelCase
struct ApiResponse {
    status_code: u16,          // → "statusCode"
    error_message: String,     // → "errorMessage"
    player_name: String,       // → "playerName"
}

// 可用的命名风格:
// "camelCase"           → statusCode
// "PascalCase"          → StatusCode
// "snake_case"          → status_code(Rust 默认,通常不需要)
// "SCREAMING_SNAKE_CASE" → STATUS_CODE
// "kebab-case"          → status-code
// 反序列化时拒绝未知字段(默认是忽略)
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Config {
    host: String,
    port: u16,
}
// 如果 JSON 里有 {"host":"...", "port":8080, "typo_field": true} → 报错

字段属性(加在字段上)

#[derive(Serialize, Deserialize)]
struct Player {
    // 重命名单个字段
    #[serde(rename = "playerId")]
    id: u32,

    // 反序列化时如果字段缺失,用默认值
    #[serde(default)]
    level: u32,  // 缺失时 = 0(u32 的 Default)

    // 自定义默认值
    #[serde(default = "default_health")]
    health: f32,

    // 序列化时跳过这个字段
    #[serde(skip_serializing)]
    password_hash: String,

    // 反序列化时跳过(用默认值)
    #[serde(skip_deserializing)]
    computed_score: f64,

    // 双向都跳过
    #[serde(skip)]
    cache: Option<Vec<u8>>,

    // 如果值是 None 就不输出这个字段(而不是输出 null)
    #[serde(skip_serializing_if = "Option::is_none")]
    nickname: Option<String>,

    // 扁平化嵌套结构
    #[serde(flatten)]
    metadata: Metadata,
}

fn default_health() -> f32 { 100.0 }

#[derive(Serialize, Deserialize)]
struct Metadata {
    created_at: String,
    updated_at: String,
}
// flatten 效果: metadata 的字段直接出现在 Player 的 JSON 中
// {"playerId":1,"level":15,"health":100.0,"created_at":"...","updated_at":"..."}
// 而不是 {"playerId":1,"metadata":{"created_at":"...","updated_at":"..."}}

枚举序列化方式

// 外部标签(默认)
#[derive(Serialize, Deserialize)]
enum GameEvent {
    PlayerJoined { player_id: u32 },
    ItemDropped { item_id: u32, position: [f32; 3] },
    MatchEnded,
}
// → {"PlayerJoined":{"player_id":42}}
// → {"ItemDropped":{"item_id":1,"position":[1.0,2.0,3.0]}}
// → "MatchEnded"

// 内部标签(推荐用于 API)
#[derive(Serialize, Deserialize)]
#[serde(tag = "type")]
enum GameEvent {
    PlayerJoined { player_id: u32 },
    ItemDropped { item_id: u32, position: [f32; 3] },
    MatchEnded,
}
// → {"type":"PlayerJoined","player_id":42}
// → {"type":"ItemDropped","item_id":1,"position":[1.0,2.0,3.0]}
// → {"type":"MatchEnded"}

// 相邻标签
#[derive(Serialize, Deserialize)]
#[serde(tag = "type", content = "data")]
enum GameEvent {
    PlayerJoined { player_id: u32 },
    Damage(u32),
}
// → {"type":"PlayerJoined","data":{"player_id":42}}
// → {"type":"Damage","data":50}

// 无标签(按内容匹配)
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
enum Value {
    Int(i64),
    Float(f64),
    Text(String),
}
// → 42       (尝试 Int → 成功)
// → 3.14     (尝试 Int → 失败,尝试 Float → 成功)
// → "hello"  (尝试 Int → 失败,Float → 失败,Text → 成功)

格式 crate 对比

serde_json — JSON

serde_json = "1"
use serde_json::{json, Value};

// 序列化
let s = serde_json::to_string(&player)?;           // 紧凑
let s = serde_json::to_string_pretty(&player)?;    // 格式化

// 反序列化
let p: Player = serde_json::from_str(&json_str)?;
let p: Player = serde_json::from_slice(&bytes)?;   // 从字节切片
let p: Player = serde_json::from_reader(file)?;    // 从 Read

// 动态 JSON(不需要结构体)
let v: Value = serde_json::from_str(r#"{"key": "value"}"#)?;
let key = v["key"].as_str().unwrap();

// json! 宏构造动态值
let v = json!({
    "error": {"code": "NOT_FOUND", "message": "player not found"}
});

用在哪:HTTP API 请求响应、配置文件、日志结构化输出。

bincode — 紧凑二进制

bincode = "1"
// 序列化
let bytes: Vec<u8> = bincode::serialize(&player)?;
// 比 JSON 小很多:Player 大约 20 字节 vs JSON 100+ 字节

// 反序列化
let player: Player = bincode::deserialize(&bytes)?;

特点

用在哪:游戏网络协议(UDP 状态同步)、进程间通信、缓存到 Redis 的二进制值。

toml — TOML 配置

toml = "0.8"
#[derive(Serialize, Deserialize)]
struct Config {
    server: ServerConfig,
    database: DatabaseConfig,
}

#[derive(Serialize, Deserialize)]
struct ServerConfig {
    host: String,
    port: u16,
}

#[derive(Serialize, Deserialize)]
struct DatabaseConfig {
    url: String,
    max_connections: u32,
}

// 读取配置文件
let content = std::fs::read_to_string("config.toml")?;
let config: Config = toml::from_str(&content)?;
# config.toml
[server]
host = "0.0.0.0"
port = 3000

[database]
url = "postgres://tarkov:123@localhost/mini_tarkov"
max_connections = 20

用在哪:Rust 项目配置文件(Cargo.toml 自己就是 TOML)。

postcard — 嵌入式 / 零拷贝二进制

postcard = { version = "1", features = ["alloc"] }
let bytes = postcard::to_allocvec(&player)?;
let player: Player = postcard::from_bytes(&bytes)?;

特点:比 bincode 更紧凑(varint 编码),支持 #![no_std]

用在哪:嵌入式设备通信、对包体积极致要求的场景。

rmp-serde — MessagePack

rmp-serde = "1"
let bytes = rmp_serde::to_vec(&player)?;
let player: Player = rmp_serde::from_slice(&bytes)?;

特点:二进制 JSON——比 JSON 小,比 bincode 跨语言。自描述(带字段名)。

用在哪:需要跨语言 + 比 JSON 高效的场景。


格式选型对比

              体积    速度    人类可读   跨语言   自描述   用在哪
─────────────────────────────────────────────────────────────────
serde_json    大      中等    ✓         ✓        ✓      HTTP API
bincode       极小    极快    ✗         ✗        ✗      Rust 内部通信/游戏协议
toml          中      慢      ✓         ✓        ✓      配置文件
postcard      极小    快      ✗         △        ✗      嵌入式
rmp(MsgPack)  小      快      ✗         ✓        ✓      跨语言高效通信
protobuf      小      快      ✗         ✓        ✓      gRPC / 严格 schema

mini_tarkov 的选择:


高级用法

自定义序列化(单个字段)

use serde::{Serialize, Deserialize, Serializer, Deserializer};
use chrono::{DateTime, Utc};

#[derive(Serialize, Deserialize)]
struct Event {
    name: String,

    // 自定义:DateTime 序列化为 Unix 时间戳
    #[serde(serialize_with = "serialize_timestamp")]
    #[serde(deserialize_with = "deserialize_timestamp")]
    time: DateTime<Utc>,
}

fn serialize_timestamp<S: Serializer>(dt: &DateTime<Utc>, s: S) -> Result<S::Ok, S::Error> {
    s.serialize_i64(dt.timestamp())
}

fn deserialize_timestamp<'de, D: Deserializer<'de>>(d: D) -> Result<DateTime<Utc>, D::Error> {
    let ts = i64::deserialize(d)?;
    DateTime::from_timestamp(ts, 0)
        .ok_or_else(|| serde::de::Error::custom("invalid timestamp"))
}

// → {"name":"match_started","time":1716300000}
// 而不是 {"name":"match_started","time":"2024-05-21T10:00:00Z"}

DeserializeOwned vs Deserialize<‘de>

// Deserialize<'de> — 可以借用输入数据(零拷贝)
fn parse<'a>(json: &'a str) -> Result<Player<'a>, Error>
// Player 里的 &str 字段直接指向 json 字符串的内存,不复制

// DeserializeOwned — 不借用输入,结果拥有所有数据
fn parse(json: &str) -> Result<Player, Error>
// Player 里是 String,独立拥有数据,json 可以随时释放

// axum 的 Json<T> 要求 T: DeserializeOwned
// 因为请求 body 读完后 buffer 就释放了,T 不能借用它

分享这篇文章:

上一篇
Rust 基础:类型获得 Trait 方法的三种方式
下一篇
Rust 基础:? 操作符