← 中文资源

简体中文版本

Rust Serde 自定义字段反序列化

通过自定义 Visitor 让同一字段同时接受空格分隔字符串和字符串数组,并保留默认值与未知字段校验。

阅读英文原文

功能说明

ServiceConfig 使用 kebab-case 重命名,allowed-origins 既可能来自环境变量式的空格分隔字符串,也可能来自 JSON 数组。deserialize_origins 实现一个 Visitor,分别处理 visit_str 与 visit_seq,把两种输入统一成 Vec。replicas 缺省为 2,deny_unknown_fields 让拼写错误的键立即失败,避免配置被静默忽略。

关键代码

deserialize_any 让格式自行决定调用哪个 visit 方法,因此同一个函数可以接受字符串或序列。visit_str 用 split_whitespace 丢弃多余空白,visit_seq 逐个读取字符串并保留顺序。字段上的 default 与 deserialize_with 可以共存:字段缺失时使用默认空向量,字段出现时才调用自定义函数。main 使用 serde_json 演示两种写法,并用 unwrap_err 验证未知字段被拒绝。

使用注意

deserialize_any 依赖自描述格式,JSON、YAML 可用,但 bincode 这类非自描述格式会报错;此类格式应使用 deserialize_str 或固定形状。Serialize 派生会把 Vec 写成数组,因此序列化结果与最初的字符串形式不同,若要求往返一致需同时实现 serialize_with。空格分隔无法表示包含空格的来源,复杂配置应统一使用数组。deny_unknown_fields 与 flatten 同时使用可能冲突。

验证方式

运行 cargo run,第一条配置解析出两个来源并打印结构体;第二条缺省 replicas 的断言通过;第三条 replica 拼写错误会返回 unknown field 错误。可增加空字符串输入,确认得到空 Vec;再尝试传入数字,验证 Visitor 的 expecting 文案出现在错误信息中。

代码示例

use std::fmt;

use serde::de::{self, Deserializer, SeqAccess, Visitor};
use serde::{Deserialize, Serialize};

fn deserialize_origins<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
where
    D: Deserializer<'de>,
{
    struct OriginsVisitor;

    impl<'de> Visitor<'de> for OriginsVisitor {
        type Value = Vec<String>;

        fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
            formatter.write_str("a space-separated string or an array of strings")
        }

        fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
        where
            E: de::Error,
        {
            Ok(value.split_whitespace().map(str::to_owned).collect())
        }

        fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
        where
            A: SeqAccess<'de>,
        {
            let mut origins = Vec::new();
            while let Some(origin) = sequence.next_element::<String>()? {
                origins.push(origin);
            }
            Ok(origins)
        }
    }

    deserializer.deserialize_any(OriginsVisitor)
}

fn default_replicas() -> u8 {
    2
}

#[derive(Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
struct ServiceConfig {
    name: String,
    #[serde(default = "default_replicas")]
    replicas: u8,
    #[serde(default, deserialize_with = "deserialize_origins")]
    allowed_origins: Vec<String>,
}

fn main() {
    let expanded = r#"{
        "name": "checkout-api",
        "replicas": 4,
        "allowed-origins": "https://shop.example https://admin.example"
    }"#;
    let config: ServiceConfig = serde_json::from_str(expanded).unwrap();
    println!("{config:?}");
    assert_eq!(config.allowed_origins.len(), 2);

    let array_form = r#"{"name":"worker","allowed-origins":["https://a.example"]}"#;
    let config: ServiceConfig = serde_json::from_str(array_form).unwrap();
    println!("default replicas: {}", config.replicas);
    assert_eq!(config.replicas, 2);

    let unknown = r#"{"name":"api","replica":3}"#;
    let error = serde_json::from_str::<ServiceConfig>(unknown).unwrap_err();
    println!("rejected unknown field: {error}");
}