PAYATHON 2026

Rust validator 如何校验双层 Option 并保留字段级错误?

数据小许

可以手动为 BookUpdateset 实现 Validate,匹配双层 Option,再通过 ValidationErrors::add("title", error) 和 add("author", error) 分别记录错误。这样可以拒绝 Some(None),调用方也能继续使用 .validate() 获取字段级错误。

为什么原来的 custom 校验没有生效?

原问题需要保留三种状态:

Rust 值请求含义title、author 的处理
None未提供字段允许,不更新
Some(None)显式传入 null拒绝
Some(Some(value))提供具体值校验字符串长度后更新

这三种状态由 serde_with::rust::double_option 的反序列化语义提供,字段上的 default 用于处理字段缺失的情况。double_option 文档

validator 的派生校验会对 Option 中存在的值执行规则。按问题中观察到的双层 Option 展开行为,custom 函数接收到的是内部字符串引用,无法检查被跳过的 Some(None)。把参数改成 &str 可以适配字符串校验,但无法检查这里的 null。validator 校验说明

原来的 schema 函数可以访问完整结构体,但它的返回类型只有 Result<(), ValidationError>,无法直接返回按多个字段组织的错误集合。另外,两次辅助函数调用之间使用了 ?,第一个字段校验失败后就会返回,无法同时收集两个字段的问题。

手动实现 Validate,按字段收集错误

先从原结构体的 derive 列表中移除 Validate,并删除对应的 #[validate(...)] 属性及 schema 校验属性。下面的示例只保留了反序列化相关属性;原有的 ToSchema、#[schema(...)] 等配置可以继续保留。

use chrono::{DateTime, Utc};
use serde::Deserialize;
use validator::{
    Validate, ValidateLength, ValidationError, ValidationErrors,
};

#[derive(Clone, Deserialize)]
pub struct BookUpdateset {
    #[serde(default, with = "::serde_with::rust::double_option")]
    pub title: Option<Option<String>>,

    #[serde(default, with = "::serde_with::rust::double_option")]
    pub author: Option<Option<String>>,

    #[serde(default, with = "::serde_with::rust::double_option")]
    pub publish_date: Option<Option<DateTime<Utc>>>,
}

fn validate_text_update(
    field_name: &'static str,
    field: &Option<Option<String>>,
    errors: &mut ValidationErrors,
) {
    match field {
        // 未提供字段:不更新,也不校验。
        None => {}

        // 显式 null:记录到对应字段。
        Some(None) => {
            let mut error = ValidationError::new("not_null");
            error.message = Some("Field cannot be null".into());
            errors.add(field_name, error);
        }

        // 提供字符串:保留原有的长度限制。
        Some(Some(value)) => {
            if !value.validate_length(Some(1), Some(1024), None) {
                let mut error = ValidationError::new("length");
                error.message = Some(
                    "Length must be between 1 and 1024".into(),
                );
                error.add_param("min".into(), &1_u64);
                error.add_param("max".into(), &1024_u64);
                errors.add(field_name, error);
            }
        }
    }
}

impl Validate for BookUpdateset {
    fn validate(&self) -> Result<(), ValidationErrors> {
        let mut errors = ValidationErrors::new();

        validate_text_update("title", &self.title, &mut errors);
        validate_text_update("author", &self.author, &mut errors);

        // publish_date 允许显式 null,不应用上述文本规则。

        if errors.is_empty() {
            Ok(())
        } else {
            Err(errors)
        }
    }
}

示例复用 ValidateLength::validate_length(min, max, equal) 进行长度范围校验,要求所用的 validator 版本提供该 trait。原问题未给出依赖版本,接入时应对照项目版本的 API。ValidateLength 文档

如何取得具体字段的错误?

ValidationErrors::add 会把错误放入对应字段的 ValidationErrorsKind::Field 中。可以直接读取 field_errors(),也可以继续遍历 errors()。ValidationErrors API 与实现

例如,以下请求中的两个字段都会被记录为错误:

{
  "title": null,
  "author": null
}

在引入 serde_json 的情况下,可以这样读取:

let updateset: BookUpdateset = serde_json::from_str(
    r#"{"title": null, "author": null}"#,
)?;

if let Err(errors) = updateset.validate() {
    for (field, field_errors) in errors.field_errors() {
        for error in field_errors {
            println!("{}: {}", field, error.code);
        }
    }
}

这段代码需要放在返回类型允许传播反序列化错误的函数中。两个字段的错误码都是 not_null。错误集合使用 HashMap,不应依赖遍历顺序。

schema 错误和 Struct 错误有什么区别?

结构体级 schema 校验错误会归到特殊键 __all__ 下。ValidationErrorsKind::Struct 用于表达嵌套结构体的错误,不能仅凭校验函数定义在结构体上,就判断错误一定属于 Struct。枚举名称也是复数形式的 ValidationErrorsKind。validator 错误结构与 schema 说明

手动调用 add 可以把错误分别挂到 title 和 author。

接入时还要注意什么?

  • 手动实现 Validate 后,不能同时保留该结构体的 #[derive(Validate)],否则会出现重复实现。
  • 不要直接添加 #[validate(required)] 代替这套规则。更新接口允许字段缺失,而 required 要求 Option 为 Some。
  • 保留 #[serde(default, with = "::serde_with::rust::double_option")],否则可能丢失字段缺失与显式 null 的区别。
  • .validate() 需要在反序列化后显式调用,或由已经配置好的请求校验流程调用;Deserialize 本身不会自动执行它。
  • publish_date 允许 null,但任意非 null 值不一定都能通过。具体日期值仍须通过 DateTime<Utc> 的反序列化,chrono 需要启用相应的 serde 支持。

备注:内容仅供参考。