首页/发现新知
视界 · 深度阅读

深入理解RUST序列化与反序列化serde + derive

‌Serde是Rust序列化的事实标准框架,核心思路是用#[derive(Serialize,Deserialize)]在编译期生成代码,而非运行时反射,因此性能接近手写实现,这篇文章介绍RUST序列化与反序列化serde+derive的相关知识,感兴趣的朋友一起看看吧

目录
  • 一、Serde 连接数据结构和数据格式
    • 完整案例
  • 二、结构体属性就是协议规则
    • 三、枚举的四种表示
      • 四、借用、生命周期与 DeserializeOwned
        • 五、flatten 与显式迁移
          • 六、自定义字段适配器和 Visitor
            • 七、完整案例:版本化任务请求
              • 八、执行过程、所有权与兼容性再深入一步
                • 1. Serde 不要求先建立通用中间树
                • 2. 序列化与反序列化的所有权方向不同
                • 3. 输入属性和输出属性要分别审查
                • 4. 缺失、null 与 Option 不是同一件事
                • 5. 借用为什么经常止步于边界层
                • 6. 兼容性变化要分类处理
                • 7. 自定义适配器不应访问业务资源
                • 8. 私有字段不等于反序列化后一定合法
                • 9. Err 会清理内存,不会撤销外部副作用
              • 九、测试应该证明什么
                • 十、常见错误
                  • 把 Deserialize 当成业务校验
                  • 所有输入都静默忽略未知字段
                  • 用 untagged 承担复杂版本演进
                  • 为了少一次分配传播复杂生命周期
                  • 先序列化秘密再做文本替换
                • 十一、Serde 与具体格式的边界
                  • 参考资料

                    把一段 JSON 读进结构体只需要一行代码。真正影响项目可靠性的,是这一行前后的约定:旧字段怎样兼容,未知字段是否拒绝,反序列化后的值能否直接进入业务层,借用的数据可以保存多久,协议升级时谁负责迁移。

                    Serde 解决 Rust 数据类型与数据格式之间的通用转换。它不负责打开文件,不限定 JSON 或 TOML,也不判断“优先级 9 是否符合业务规则”。本文使用 Rust 1.94.0、edition 2024、serde 1.0.229、serde_json 1.0.151、serde_test 1.0.177 和 thiserror 2.0.21。

                    serde = { version = "=1.0.229", features = ["derive"] }
                    

                    一、Serde 连接数据结构和数据格式

                    Rust 值
                      └─ Serialize
                           └─ Serializer(具体格式实现)
                                └─ 文本或字节
                    文本或字节
                      └─ Deserializer(具体格式实现)
                           └─ Deserialize
                                └─ Rust 值

                    Serialize 由数据类型实现,它通过 Serializer 的结构体、序列、枚举、数字和字符串等接口描述自身。Deserialize 从 Deserializer 提供的数据构造 Rust 值。JSON、TOML 等 crate 实现格式端;结构体和枚举实现数据端。

                    #[derive(Serialize, Deserialize)] 是编译期过程宏。它生成 trait 实现,不在运行时反射字段。类型没有实现相应 trait 时,代码会在编译期失败。

                    完整案例

                    //! 本例验证 Serde 的数据结构 trait 与具体数据格式彼此分离。
                    //! 正常路径把同一个结构体序列化为 JSON;测试直接观察格式无关的 token。
                    use serde::{Deserialize, Serialize};
                    /// 具名字段结构体会通过 Serializer 的“结构体”接口输出字段名和值。
                    #[derive(Debug, PartialEq, Serialize, Deserialize)]
                    struct Task {
                        id: u64,
                        title: String,
                    }
                    /// newtype 在 Rust 中是独立类型,Serde 数据模型也保留它的类型名称。
                    #[derive(Debug, PartialEq, Serialize, Deserialize)]
                    struct TaskId(u64);
                    /// tuple struct 使用位置而不是字段名表达成员。
                    #[derive(Debug, PartialEq, Serialize, Deserialize)]
                    struct Point(i32, i32);
                    /// unit struct 没有需要携带的字段。
                    #[derive(Debug, PartialEq, Serialize, Deserialize)]
                    struct End;
                    /// `main` 只选择 JSON 作为一种可见格式;derive 出来的 trait 实现没有绑定 JSON。
                    fn main() -> Result {
                        let task = Task {
                            id: 7,
                            title: "整理测试".to_owned(),
                        };
                        // `to_string` 借用 task,所以序列化后仍可继续使用原值。
                        let json = serde_json::to_string(&task)?;
                        println!("JSON: {json}");
                        // `from_str` 返回新的 Task;其中 String 拥有解析所得文本。
                        let decoded: Task = serde_json::from_str(&json)?;
                        println!("恢复: id={}, title={}", decoded.id, decoded.title);
                        println!(
                            "其他形状: newtype={}, tuple={}, unit={}",
                            serde_json::to_string(&TaskId(9))?,
                            serde_json::to_string(&Point(2, 3))?,
                            serde_json::to_string(&End)?
                        );
                        Ok(())
                    }

                    Task 传给 to_string 时只被借用,原值仍可使用。from_str 创建新的 Task,其中的 String 拥有解析结果。运行输出是:

                    JSON: {"id":7,"title":"整理测试"}
                    恢复: id=7, title=整理测试
                    其他形状: newtype=9, tuple=[2,3], unit=null
                    

                    最后一行只是 JSON 对这些 Rust 形状的表示。serde_test 能直接检查 Serde token,适合验证不绑定某一种文本格式的数据模型。

                    二、结构体属性就是协议规则

                    rename 改一个外部名称,rename_all 批量转换名称。alias 只放宽输入,重新输出仍使用当前名称,因此适合“读旧写新”。

                    default 只处理字段缺失。字段明确为 null 时,Deserializer 仍会把 null 转成目标类型;普通 String 不能接收。字段存在但类型错误时,default 也不会掩盖错误。

                    skip_serializing_if 根据值决定是否输出。skip_serializing 和 skip_deserializing 只影响一个方向。秘密字段即使跳过序列化,也仍可能被 Debug 或日志输出,必须分别审查。

                    deny_unknown_fields 能尽早发现拼错字段,适合工具参数和启动配置。允许第三方扩展的事件协议则可能需要扩展区。严格和宽松都必须是明确的协议选择。

                    //! 本例集中验证常用结构体属性。
                    //! 正常路径读取旧字段名并补默认值;失败注入分别传入未知字段、null 和错误类型。
                    use serde::{Deserialize, Serialize};
                    fn default_priority() -> u8 {
                        3
                    }
                    fn is_default_priority(value: &u8) -> bool {
                        *value == default_priority()
                    }
                    /// 输入严格拒绝拼错的字段;旧字段名 `task_name` 只作为反序列化别名。
                    #[derive(Debug, PartialEq, Serialize, Deserialize)]
                    #[serde(rename_all = "camelCase", deny_unknown_fields)]
                    struct TaskDto {
                        id: u64,
                        #[serde(alias = "task_name")]
                        name: String,
                        #[serde(
                            default = "default_priority",
                            skip_serializing_if = "is_default_priority"
                        )]
                        priority: u8,
                        #[serde(default, skip_serializing_if = "Vec::is_empty")]
                        labels: Vec,
                        #[serde(skip_serializing, default)]
                        access_token: String,
                    }
                    /// transparent 让包装类型在线格式中直接表现为内部字符串。
                    #[derive(Debug, PartialEq, Serialize, Deserialize)]
                    #[serde(transparent)]
                    struct RequestId(String);
                    fn main() -> Result {
                        let old_input = r#"{"id":7,"task_name":"build","accessToken":"secret"}"#;
                        let task: TaskDto = serde_json::from_str(old_input)?;
                        println!("读取: name={}, priority={}", task.name, task.priority);
                        let output = serde_json::to_string(&task)?;
                        println!("输出: {output}");
                        println!(
                            "transparent: {}",
                            serde_json::to_string(&RequestId("req-9".into()))?
                        );
                        for input in [
                            r#"{"id":7,"name":"build","typo":1}"#,
                            r#"{"id":7,"name":null}"#,
                            r#"{"id":7,"name":"build","priority":"high"}"#,
                        ] {
                            let error = serde_json::from_str::(input).expect_err("故意失败输入应被拒绝");
                            println!("拒绝: {error}");
                        }
                        Ok(())
                    }

                    本机运行会分别报告未知字段、null 和类型错误。默认优先级不会输出,旧名称 task_name 只在输入端有效:

                    读取: name=build, priority=3
                    输出: {"id":7,"name":"build"}
                    transparent: "req-9"
                    拒绝: unknown field typo
                    拒绝: invalid type: null, expected a string
                    拒绝: invalid type: string "high", expected u8
                    

                    transparent 让单字段包装类型在线格式中直接表现为内部值。Rust 侧仍有独立的 RequestId 类型,外部无需增加一层对象。

                    三、枚举的四种表示

                    默认的 externally tagged 把 variant 名放在最外层,适用范围最广。internally tagged 用 tag 把标签放进对象,便于人读,但不能容纳 tuple variant。adjacently tagged 用独立的 tag 与 content 字段,能稳定表达更多 variant 形状。

                    untagged 没有判别字段。Serde 按声明顺序尝试,第一种成功的类型胜出。形状重叠时,调整顺序可能改变含义;variant 多时还会重复解析并给出较弱错误。它适合少量历史兼容输入,不适合持续扩大的命令协议。

                    //! 本例用同一个业务含义比较四种枚举表示。
                    //! 故意失败路径展示内部标签的未知变体;测试说明 untagged 按声明顺序尝试。
                    use serde::{Deserialize, Serialize};
                    #[derive(Debug, PartialEq, Serialize, Deserialize)]
                    enum External {
                        Start { id: u64 },
                        Stop,
                    }
                    #[derive(Debug, PartialEq, Serialize, Deserialize)]
                    #[serde(tag = "kind", rename_all = "snake_case")]
                    enum Internal {
                        Start {
                            id: u64,
                        },
                        Stop,
                        #[serde(other)]
                        Unknown,
                    }
                    #[derive(Debug, PartialEq, Serialize, Deserialize)]
                    #[serde(tag = "kind", content = "data", rename_all = "snake_case")]
                    enum Adjacent {
                        Start { id: u64 },
                        Message(String),
                    }
                    #[derive(Debug, PartialEq, Serialize, Deserialize)]
                    #[serde(untagged)]
                    enum LooseNumber {
                        Integer(u64),
                        Text(String),
                    }
                    fn main() -> Result {
                        println!(
                            "external: {}",
                            serde_json::to_string(&External::Start { id: 7 })?
                        );
                        println!(
                            "internal: {}",
                            serde_json::to_string(&Internal::Start { id: 7 })?
                        );
                        println!(
                            "adjacent: {}",
                            serde_json::to_string(&Adjacent::Message("ready".into()))?
                        );
                        println!(
                            "untagged: {:?}",
                            serde_json::from_str::(r#""42""#)?
                        );
                        let unknown: Internal = serde_json::from_str(r#"{"kind":"pause"}"#)?;
                        println!("未知内部标签: {unknown:?}");
                        Ok(())
                    }

                    #[serde(other)] 可接住未知标签,但只适用于 internally tagged 或 adjacently tagged 的 unit variant。事件消费者可以选择忽略未知事件;命令执行入口若不知道含义,应拒绝执行。

                    external: {"Start":{"id":7}}
                    internal: {"kind":"start","id":7}
                    adjacent: {"kind":"message","data":"ready"}
                    untagged: Text("42")
                    未知内部标签: Unknown
                    

                    四、借用、生命周期与 DeserializeOwned

                    Deserialize 中的生命周期表示结果最多可以从输入借用多久。输入是仍然存活的字符串或字节切片时,某些字段可以直接引用输入,减少分配;返回值不能超过输入寿命。

                    DeserializeOwned 要求结果不借用反序列化器的数据。文件 reader、网络流或会复用内部缓冲的接口通常需要它,因为函数返回后临时缓冲可能销毁。

                    即使从字符串解析,也不是所有内容都能借用。带转义的 JSON 字符串必须先解码,Cow 会变成 Owned。借用是一种条件满足时的优化,不是业务正确性保证。

                    //! 本例验证反序列化结果何时能借用输入,何时必须拥有数据。
                    //! `BorrowedTask` 不能活得比 JSON 字符串更久;reader 入口返回 `OwnedTask`。
                    use std::{borrow::Cow, io::Cursor};
                    use serde::{Deserialize, Serialize, de::DeserializeOwned};
                    #[derive(Debug, Deserialize)]
                    struct BorrowedTask {
                        id: u64,
                        #[serde(borrow)]
                        title: Cow,
                    }
                    #[derive(Debug, Deserialize, Serialize)]
                    struct OwnedTask {
                        id: u64,
                        title: String,
                    }
                    /// 该泛型函数可从任意借用期的字符串读取 T;T 可以借用 `input`。
                    fn parse_borrowed(input: &'de str) -> Result
                    where
                        T: Deserialize,
                    {
                        serde_json::from_str(input)
                    }
                    /// reader 内部缓冲不会交给返回值长期保存,因此结果要求 `DeserializeOwned`。
                    fn parse_reader(bytes: &[u8]) -> Result
                    where
                        T: DeserializeOwned,
                    {
                        serde_json::from_reader(Cursor::new(bytes))
                    }
                    fn main() -> Result {
                        let input = r#"{"id":7,"title":"borrowed text"}"#;
                        let borrowed: BorrowedTask = parse_borrowed(input)?;
                        println!(
                            "借用解析: id={}, borrowed={}",
                            borrowed.id,
                            matches!(borrowed.title, Cow::Borrowed(_))
                        );
                        let owned: OwnedTask = parse_reader(input.as_bytes())?;
                        println!("reader 解析: id={}, title={}", owned.id, owned.title);
                        Ok(())
                    }

                    T: Deserialize 允许 T 借用调用者输入;T: DeserializeOwned 适合返回独立值。Deserialize 通常是错误约束,它不是“让结果活得更久”,而是要求借用永久输入。

                    借用解析: id=7, borrowed=true
                    reader 解析: id=7, title=borrowed text
                    

                    derive 会自动推导泛型 bound。只有自动结果过严或不正确时才使用 #[serde(bound = "...")],否则手写 bound 只会增加维护负担。

                    五、flatten 与显式迁移

                    flatten 可以把公共字段提升到外层,也可以把未知字段收进 Map。后一种方式适合明确允许扩展的元数据。它不能与 deny_unknown_fields 组合:一个要求接收未知字段,另一个要求拒绝,两条规则相冲突。

                    alias 和 default 解决表示兼容;字段含义、单位或支持版本变化必须进入显式迁移函数。反序列化成功只证明输入形状可读,不证明业务值可执行。

                    //! 本例演示 flatten 的两个常见用途,以及旧版线格式到当前领域类型的显式迁移。
                    //! 扩展字段会被收集;不支持的协议版本和非法名称是故意失败路径。
                    use std::collections::BTreeMap;
                    use serde::{Deserialize, Serialize};
                    use serde_json::Value;
                    use thiserror::Error;
                    #[derive(Debug, PartialEq, Serialize, Deserialize)]
                    struct CommonFields {
                        request_id: String,
                    }
                    #[derive(Debug, PartialEq, Serialize, Deserialize)]
                    struct ExtensibleRequest {
                        #[serde(flatten)]
                        common: CommonFields,
                        action: String,
                        #[serde(flatten)]
                        extensions: BTreeMap,
                    }
                    fn legacy_version() -> u16 {
                        1
                    }
                    #[derive(Debug, Deserialize)]
                    struct WireTask {
                        #[serde(default = "legacy_version")]
                        version: u16,
                        #[serde(alias = "task_name")]
                        name: String,
                    }
                    #[derive(Debug, PartialEq)]
                    struct Task {
                        name: String,
                        migrated_from: u16,
                    }
                    #[derive(Debug, Error, PartialEq)]
                    enum MigrationError {
                        #[error("不支持协议版本 {0}")]
                        UnsupportedVersion(u16),
                        #[error("任务名称去除首尾空白后不能为空")]
                        EmptyName,
                    }
                    fn migrate(wire: WireTask) -> Result {
                        if !matches!(wire.version, 1 | 2) {
                            return Err(MigrationError::UnsupportedVersion(wire.version));
                        }
                        let name = wire.name.trim().to_owned();
                        if name.is_empty() {
                            return Err(MigrationError::EmptyName);
                        }
                        Ok(Task {
                            name,
                            migrated_from: wire.version,
                        })
                    }
                    fn main() -> Result<(), Box> {
                        let request: ExtensibleRequest = serde_json::from_str(
                            r#"{"request_id":"r-1","action":"run","trace_id":"t-9","dry_run":true}"#,
                        )?;
                        println!(
                            "扩展字段: {:?}",
                            request.extensions.keys().collect::<Vec>()
                        );
                        let old: WireTask = serde_json::from_str(r#"{"task_name":"  build  "}"#)?;
                        println!("迁移结果: {:?}", migrate(old)?);
                        let future: WireTask = serde_json::from_str(r#"{"version":9,"name":"build"}"#)?;
                        println!("拒绝未来版本: {}", migrate(future).unwrap_err());
                        Ok(())
                    }

                    WireTask 是输入模型,Task 是领域模型。migrate 按值接收 wire,取得字符串所有权;成功时把整理后的值移入新对象,失败时不生成领域任务。

                    扩展字段: ["dry_run", "trace_id"]
                    迁移结果: Task { name: "build", migrated_from: 1 }
                    拒绝未来版本: 不支持协议版本 9
                    

                    六、自定义字段适配器和 Visitor

                    普通字段优先使用 derive。线格式和领域类型确实不一致时,再用 with、serialize_with 或 deserialize_with。适配器应先读一个明确的中间类型,再做格式与范围检查,错误通过 de::Error::custom 返回。

                    //! 本例用字段适配器为 TaskId 定义稳定线格式,并用 Visitor 兼容两种旧输入。
                    //! 错误前缀、零 ID 和非数字文本都是故意失败路径。
                    use std::fmt;
                    use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
                    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
                    struct TaskId(u64);
                    mod task_id_format {
                        use super::*;
                        pub fn serialize(value: &TaskId, serializer: S) -> Result
                        where
                            S: Serializer,
                        {
                            serializer.serialize_str(&format!("task-{}", value.0))
                        }
                        pub fn deserialize(deserializer: D) -> Result
                        where
                            D: Deserializer,
                        {
                            let text = String::deserialize(deserializer)?;
                            let digits = text
                                .strip_prefix("task-")
                                .ok_or_else(|| de::Error::custom("任务 ID 必须以 task- 开头"))?;
                            let value = digits
                                .parse::()
                                .map_err(|_| de::Error::custom("task- 后面必须是无符号整数"))?;
                            if value == 0 {
                                return Err(de::Error::custom("任务 ID 必须大于 0"));
                            }
                            Ok(TaskId(value))
                        }
                    }
                    #[derive(Debug, PartialEq, Serialize, Deserialize)]
                    struct TaskRef {
                        #[serde(with = "task_id_format")]
                        id: TaskId,
                    }
                    #[derive(Debug, PartialEq)]
                    struct StringOrU64(u64);
                    struct StringOrU64Visitor;
                    impl de::Visitor for StringOrU64Visitor {
                        type Value = StringOrU64;
                        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
                            formatter.write_str("无符号整数或只包含数字的字符串")
                        }
                        fn visit_u64(self, value: u64) -> Result {
                            Ok(StringOrU64(value))
                        }
                        fn visit_str(self, value: &str) -> Result
                        where
                            E: de::Error,
                        {
                            value.parse::().map(StringOrU64).map_err(E::custom)
                        }
                    }
                    impl Deserialize for StringOrU64 {
                        fn deserialize(deserializer: D) -> Result
                        where
                            D: Deserializer,
                        {
                            deserializer.deserialize_any(StringOrU64Visitor)
                        }
                    }
                    fn main() -> Result {
                        let json = serde_json::to_string(&TaskRef { id: TaskId(42) })?;
                        println!("编码: {json}");
                        println!("解码: {:?}", serde_json::from_str::(&json)?);
                        for input in [r#"{"id":"job-42"}"#, r#"{"id":"task-0"}"#] {
                            println!(
                                "拒绝: {}",
                                serde_json::from_str::(input).unwrap_err()
                            );
                        }
                        println!(
                            "兼容旧数字字符串: {:?}",
                            serde_json::from_str::(r#""17""#)?
                        );
                        Ok(())
                    }

                    TaskId(42) 的 Rust 表示与 task-42 的线格式分开。手写 Visitor 则明确列出能接收的输入种类。示例使用 deserialize_any,这要求格式能够先识别值的种类;某些非自描述二进制格式并不支持。因此 Visitor 是边界工具,不是 derive 的常规替代品。

                    编码: {"id":"task-42"}
                    解码: TaskRef { id: TaskId(42) }
                    拒绝: 任务 ID 必须以 task- 开头
                    拒绝: 任务 ID 必须大于 0
                    兼容旧数字字符串: StringOrU64(17)
                    

                    remote derive 可为不能直接修改的外部类型声明镜像定义,但要持续维护字段对应。简单 newtype 或字段适配器通常更容易看懂。

                    七、完整案例:版本化任务请求

                    可靠的输入边界最好分两步:

                    外部格式
                       ↓
                    TaskRequestWire
                    - 接受历史别名
                    - 补表示层默认值
                    - 拒绝未知字段
                       ↓ normalize_wire
                    TaskRequest
                    - 版本已支持
                    - ID、名称和数值已校验
                    //! 版本化任务请求协议。
                    //! `TaskRequestWire` 只表示可接受的线格式;`normalize_wire` 迁移版本并产生已校验的领域值。
                    use serde::{Deserialize, Serialize};
                    use thiserror::Error;
                    fn legacy_version() -> u16 {
                        1
                    }
                    fn default_priority() -> Priority {
                        Priority(3)
                    }
                    /// 优先级在线上是数字。私有字段阻止业务代码随意构造,但反序列化仍可能得到越界值,
                    /// 因而必须在 `normalize_wire` 中校验。
                    #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
                    #[serde(transparent)]
                    pub struct Priority(u8);
                    impl Priority {
                        pub fn get(self) -> u8 {
                            self.0
                        }
                    }
                    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
                    #[serde(deny_unknown_fields)]
                    pub struct CreateTask {
                        #[serde(default, skip_serializing_if = "Option::is_none")]
                        pub details: Option,
                    }
                    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
                    #[serde(tag = "kind", content = "data", rename_all = "snake_case")]
                    pub enum TaskAction {
                        Create(CreateTask),
                        Cancel { task_id: String },
                        Status { task_id: String },
                    }
                    /// 线格式保留兼容属性。成功反序列化后还不能直接执行业务操作。
                    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
                    #[serde(deny_unknown_fields)]
                    pub struct TaskRequestWire {
                        #[serde(default = "legacy_version")]
                        pub version: u16,
                        pub request_id: String,
                        #[serde(alias = "task_name")]
                        pub name: String,
                        #[serde(default = "default_priority")]
                        pub priority: Priority,
                        #[serde(default, skip_serializing_if = "Vec::is_empty")]
                        pub labels: Vec,
                        pub action: TaskAction,
                    }
                    /// 领域类型中的字符串已经去除无意义空白,版本也已迁移到当前语义。
                    #[derive(Debug, Clone, PartialEq, Eq)]
                    pub struct TaskRequest {
                        pub request_id: String,
                        pub name: String,
                        pub priority: u8,
                        pub labels: Vec,
                        pub action: TaskAction,
                    }
                    #[derive(Debug, Error, PartialEq, Eq)]
                    pub enum ContractError {
                        #[error("不支持协议版本 {version}")]
                        UnsupportedVersion { version: u16 },
                        #[error("request_id 必须以 req- 开头,后面至少有一个字符")]
                        InvalidRequestId,
                        #[error("任务名称去除首尾空白后必须为 1 到 120 个字符")]
                        InvalidName,
                        #[error("优先级必须在 1 到 5 之间")]
                        InvalidPriority,
                        #[error("任务 ID 必须采用 task- 格式")]
                        InvalidTaskId,
                    }
                    /// 把可兼容读取的 wire 值转成当前领域值。
                    ///
                    /// 参数按值传入,因此函数取得其中 String 和 Vec 的所有权,无需复制整个请求。
                    /// 任何校验失败都会返回结构化 `ContractError`,调用者不得执行 action。
                    pub fn normalize_wire(wire: TaskRequestWire) -> Result {
                        if !matches!(wire.version, 1 | 2) {
                            return Err(ContractError::UnsupportedVersion {
                                version: wire.version,
                            });
                        }
                        if !valid_prefixed_number(&wire.request_id, "req-") {
                            return Err(ContractError::InvalidRequestId);
                        }
                        let name = wire.name.trim().to_owned();
                        if name.is_empty() || name.chars().count() > 120 {
                            return Err(ContractError::InvalidName);
                        }
                        if !(1..=5).contains(&wire.priority.0) {
                            return Err(ContractError::InvalidPriority);
                        }
                        if let TaskAction::Cancel { task_id } | TaskAction::Status { task_id } = &wire.action
                            && !valid_prefixed_number(task_id, "task-")
                        {
                            return Err(ContractError::InvalidTaskId);
                        }
                        // 版本 1 的标签历史上不区分大小写;迁移时统一为小写。
                        let mut labels = wire.labels;
                        if wire.version == 1 {
                            for label in &mut labels {
                                *label = label.trim().to_lowercase();
                            }
                        } else {
                            for label in &mut labels {
                                *label = label.trim().to_owned();
                            }
                        }
                        labels.retain(|label| !label.is_empty());
                        Ok(TaskRequest {
                            request_id: wire.request_id,
                            name,
                            priority: wire.priority.0,
                            labels,
                            action: wire.action,
                        })
                    }
                    fn valid_prefixed_number(value: &str, prefix: &str) -> bool {
                        value
                            .strip_prefix(prefix)
                            .is_some_and(|digits| !digits.is_empty() && digits.parse::().is_ok())
                    }

                    演示入口:

                    //! 演示版本化任务协议的正常路径和业务失败路径。
                    use versioned_task_contract::{TaskRequestWire, normalize_wire};
                    fn main() -> Result {
                        let legacy = r#"
                        {
                          "request_id": "req-7",
                          "task_name": "  build release  ",
                          "labels": [" CI ", "Rust"],
                          "action": { "kind": "create", "data": { "details": "nightly" } }
                        }
                        "#;
                        let wire: TaskRequestWire = serde_json::from_str(legacy)?;
                        println!(
                            "正常: {:#?}",
                            normalize_wire(wire).expect("示例输入应通过业务校验")
                        );
                        let future = r#"
                        {
                          "version": 9,
                          "request_id": "req-8",
                          "name": "build",
                          "action": { "kind": "status", "data": { "task_id": "task-3" } }
                        }
                        "#;
                        let wire: TaskRequestWire = serde_json::from_str(future)?;
                        println!("拒绝: {}", normalize_wire(wire).unwrap_err());
                        Ok(())
                    }

                    版本 1 的标签迁移为小写,版本 2 保留大小写但去除空白。版本分支是显式的,不通过字段是否存在猜测历史版本。Handler、任务执行器和存储层只接收 TaskRequest,不用再次猜测输入是否已检查。

                    正常: TaskRequest {
                        request_id: "req-7",
                        name: "build release",
                        priority: 3,
                        labels: [
                            "ci",
                            "rust",
                        ],
                        action: Create(
                            CreateTask {
                                details: Some(
                                    "nightly",
                                ),
                            },
                        ),
                    }
                    拒绝: 不支持协议版本 9

                    八、执行过程、所有权与兼容性再深入一步

                    1. Serde 不要求先建立通用中间树

                    直接调用 serde_json::to_string(&task) 时,derive 生成的 Serialize::serialize 会调用 JSON Serializer 的结构体接口:声明字段数,逐个交付字段名和值,然后结束结构体。它不必先把 Task 变成 serde_json::Value。

                    反序列化同理。Deserializer 读取 token,derive 生成的 Visitor 根据字段名填充结构体,检查重复字段、缺失字段和类型错误。直接解析强类型通常比“先 Value、再 from_value”少一层中间表示,错误也更接近输入边界。

                    数据类型与格式虽然解耦,组合能力仍受格式约束。例如 Serde 的 map 键可以是多种类型,JSON object 的键却只能是字符串。实现了 Serialize 只表示类型能向通用 Serializer 描述自己,不保证任意具体值都能被每一种格式表达。

                    2. 序列化与反序列化的所有权方向不同

                    序列化通常借用 &self。Serializer 读取字段,不取得业务值所有权,同一个值可以先写文件,再写网络或交给测试 Serializer。反序列化必须创建新值:String、Vec 等字段拥有解析结果,借用字段则把生命周期绑定到输入。

                    这也是输入 DTO 经常按值传给迁移函数的原因。解析阶段创建 wire 值,迁移阶段取得它的所有权,把能复用的 String 和 Vec 直接移动到领域值。输出 DTO 则可以借用领域对象,避免为了序列化再复制一份业务数据。

                    3. 输入属性和输出属性要分别审查

                    字段可能只用于输入,也可能只用于输出。skip_serializing 让字段不出现在输出,但反序列化时仍然需要它,除非另有 default。skip_deserializing 不接收外部值,因此必须有办法构造字段。skip 同时影响两个方向,只适合真正的内部状态。

                    alias 通常比“输入和输出使用两套批量命名规则”更适合迁移。外部仍能发送旧名称,服务端只输出当前名称,协议不会继续制造新的旧格式。

                    默认值也要区分 Rust 默认和协议默认。Vec 的空值经常合理,usize 的 0 对 workers 却可能表示程序永远不工作。业务默认应使用有名称的函数,使代码审查者能看到来源。

                    4. 缺失、null 与 Option 不是同一件事

                    Option 不会把所有错误变成 None。字段缺失可以得到 None;JSON 明确提供 null 也可以得到 None;字段存在但内部类型错误仍然是 Err。

                    PATCH 接口有时必须区分三种状态:字段没出现时保留旧值,字段为 null 时清空,字段有值时替换。单层 Option 不足以表达三态,可以使用 Option<Option> 或专门枚举。TOML 没有 null,同一个 patch 类型跨 JSON 与 TOML 使用时必须重新检查语义。

                    5. 借用为什么经常止步于边界层

                    HTTP handler 可以从请求体缓冲借用字符串;值一旦进入 Tokio 任务、channel、缓存或长期存储,输入缓冲通常不再活得足够久,最终仍需 String。是否借用应取决于数据存活范围:

                    • 临时检查大输入并立即返回:借用可能节省分配;
                    • 任务进入队列,晚于请求结束执行:使用拥有所有权的数据;
                    • 公共库同步分析调用者字符串:可以返回带生命周期的视图;
                    • 领域对象要持久化或缓存:不要依赖原始输入继续存活。

                    借用类型还会延长整块输入的生命。即使只保存一个很短的 &str,原始大缓冲也不能释放。内存收益要观察整个生命周期,而不是只看解析时有没有复制。

                    6. 兼容性变化要分类处理

                    新增带默认值的字段通常能读取旧输入,但旧消费者是否能读取新输出,取决于它是否拒绝未知字段。字段改名可在迁移期保留 alias;删除字段前要确认旧生产者是否仍会发送。字段含义变化最危险,即使 Rust 类型没变,也应提升协议版本。

                    版本迁移应集中在明确路径中:

                    1. 外部格式产生 wire 类型;
                    2. 迁移函数识别版本并转换历史语义;
                    3. 业务校验产生领域类型;
                    4. 内部代码只处理当前语义。

                    这样失败可以准确分成格式错误、版本不支持和业务约束不满足。数据库、handler 和任务执行器不需要各自保存一份兼容逻辑。

                    7. 自定义适配器不应访问业务资源

                    适配器适合固定前缀、时间文本和数字编码等线格式约束。依赖数据库、当前用户或另一个字段的规则不应塞进 deserialize_with:Deserializer 没有这些上下文,错误也难以分类。

                    跨字段约束如 start_at < end_at 应在 wire 构造后检查。序列化适配器还应与反序列化器成对测试:合法值能 round-trip,非法前缀、边界数字和错误类型分别失败。

                    8. 私有字段不等于反序列化后一定合法

                    最终案例的 Priority 内部字段是私有的,普通业务代码不能随意构造 Priority(99)。derive 生成的 Deserialize 仍能从线格式构造内部 u8,所以 normalize 必须检查 1 到 5。

                    如果范围永远与类型绑定,可以为 Priority 手写 Deserialize,使非法值根本无法存在。如果不同协议版本允许不同范围,让 wire 暂时保存原值、再按版本校验更灵活。两种做法的关键都是不要在类型已经进入业务层后继续假设。

                    9. Err 会清理内存,不会撤销外部副作用

                    normalize_wire 按值取得 wire。中途返回 Err 时,尚未移动的字段和临时 Vec、String 会按 RAII 释放,不会向调用者交付半个领域对象。调用者可以放心用 ? 传播错误。

                    但如果校验函数已经写文件、发请求或提交数据库,返回 Err 不会回滚这些动作。规范化与业务校验最好保持纯函数,副作用放在完整领域值形成之后。

                    九、测试应该证明什么

                    不同层次需要不同证据:

                    • 用 serde_test 验证类型调用的通用 Serde 数据模型;
                    • 用具体格式测试字段名、枚举标签、null 和数字能力;
                    • 用失败测试验证未知字段、错误类型、越界值与未来版本;
                    • 用 round-trip 证明当前实现能读取自己的输出;
                    • 直接匹配领域错误变体,不依赖完整英文解析消息;
                    • 单独检查秘密不会出现在 Serialize、Debug 和日志输出。

                    Round-trip 不能证明旧消费者能读取新输出,也不能证明另一种语言使用相同数字范围。跨版本协议仍需固定 fixture 和契约测试。

                    十、常见错误

                    把 Deserialize 当成业务校验

                    u8 只保证 0 到 255,不能保证业务范围 1 到 5。PathBuf 只保存路径数据,不能证明路径存在、是目录或允许访问。表示层和业务层要分别验证。

                    所有输入都静默忽略未知字段

                    拼错重试字段后回退默认值,可能让程序以错误策略运行。严格输入应该尽早失败;需要扩展时应提供明确扩展字段。

                    用 untagged 承担复杂版本演进

                    variant 增多后会出现顺序依赖和含糊错误。稳定命令协议更适合显式 tag 与版本号。

                    为了少一次分配传播复杂生命周期

                    边界层借用确实有价值,但进入队列、缓存和异步任务前转成拥有所有权的数据通常更稳妥。不要把生命周期复杂度传播到整个业务模型。

                    先序列化秘密再做文本替换

                    秘密应从 DTO、Serialize、Debug 和日志字段多个入口排除。事后字符串替换无法可靠覆盖嵌套结构。

                    十一、Serde 与具体格式的边界

                    Serde 提供 trait、derive、属性和通用数据模型。serde_json 负责 JSON 文本、动态 Value、流式读取和 JSON 错误;toml 负责 TOML 表、日期时间、配置语法和 span。格式能力并不完全相同:TOML 没有 null,JSON 没有原生日期时间类型。

                    derive 减少机械代码,不替开发者决定兼容策略、业务约束和安全边界。稳定做法是先定义 wire 协议,再通过显式迁移和校验进入领域类型。

                    参考资料

                    • Serde 1.0.229
                    • Serde 属性
                    • 枚举表示
                    • Deserialize 生命周期
                    • 手写 Deserialize
                    • serde_test 1.0.177

                    到此这篇关于RUST序列化与反序列化 serde + derive 详解的文章就介绍到这了,更多相关rust错误处理thiserror内容请搜索爱读视界以前的文章或继续浏览下面的相关文章希望大家以后多多支持爱读视界!

                    您可能感兴趣的文章:

                    • Rust中使用Serde对json数据进行反序列化
                    • Rust 中的 JSON 处理利器serde_json功能详解
                    • 详解Rust中#[derive]属性怎么使用
                    • Rust中的derive属性示例详解
                    读到这里,发现更多一点。探索更多内容 ↗