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 支持。
备注:内容仅供参考。
