Rust验证器:嵌套的Option字段

编程语言 2026-07-11

下面这个结构体用于映射更新实体所需的载荷:

#[derive(Clone, Deserialize, ToSchema, Validate)]
pub struct BookUpdateset {
    #[cfg_attr(
        any(feature = "test-helpers", test),
        serde(skip_serializing_if = "Option::is_none")
    )]
    #[schema(example = "Demian")]
    #[serde(default, with = "::serde_with::rust::double_option")]
    #[validate(length(min = 1, max = 1024))]
    pub title: Option<Option<String>>,
    #[cfg_attr(
        any(feature = "test-helpers", test),
        serde(skip_serializing_if = "Option::is_none")
    )]
    #[schema(example = "Hermann Hesse")]
    #[serde(default, with = "::serde_with::rust::double_option")]
    #[validate(length(min = 1, max = 1024))]
    pub author: Option<Option<String>>,
    #[cfg_attr(
        any(feature = "test-helpers", test),
        serde(skip_serializing_if = "Option::is_none")
    )]
    #[serde(default, with = "::serde_with::rust::double_option")]
    pub publish_date: Option<Option<DateTime<Utc>>>,
}

使用“双选项模式”的原因是:若收到的JSON中没有某个字段,则不对该字段进行更新;但如果该字段被显式设为null,就将其设为null。然而只有 publish_date 字段可以为null。

我想用 validator crate验证,确保 authortitle不会被显式设为null,也就是说它们不能是 Some(None)

然而,当使用自定义函数时,validator会对最内层的类型进行解包。它试图把 &&String 传给该函数,而不是传给双选项(double option)。

我发现的变通办法是在结构体级别进行验证,在结构体顶部放置一个 #[validate(schema(function = "validate_required_fields"))],并且

fn validate_required_fields(updateset: &BookUpdateset) -> Result<(), ValidationError> {
    helpers::validate_not_null(&updateset.title)?;
    helpers::validate_not_null(&updateset.author)?;

    Ok(())
}

其中

pub fn validate_not_null<T>(field: &Option<Option<T>>) -> Result<(), ValidationError> {
    if let Some(None) = field {
        return Err(ValidationError::new("Field cannot be null"));
    }

    Ok(())
}

这样做的问题是,最终的 ValidationError 无法保留是哪个字段未通过验证的详细信息,因为这个信息是在结构体级别出现的。需要澄清的是,这个细节只有在遍历 ValidationErrors 结构体时才会被暴露。我认为这个错误的类型会是 ValidationErrorKind::Struct,而不是 ValidationErrorKind::Field

我希望能够实现类似这样的做法:

#[validate(length(min = 1, max = 1024), custom(function = "helpers::validate_not_null"))]
pub author: Option<Option<String>>,

但正如前面所说,它并不起作用。

你会怎么解决这个问题?

解决方案

有趣的变通办法——一些对某些类型有特殊处理的派生宏,通常在语法层面(也就是仅凭令牌)就会判断到这一点——因此你可以使用类型别名来规避它:

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

type DoubleOption<T> = Option<Option<T>>;

#[derive(Clone, Deserialize, Validate)]
pub struct BookUpdateset {
    #[serde(default, with = "::serde_with::rust::double_option")]
    #[validate(length(min = 1, max = 1024), custom(function = "validate_not_null"))]
    pub title: DoubleOption<String>,
    #[serde(default, with = "::serde_with::rust::double_option")]
    #[validate(length(min = 1, max = 1024), custom(function = "validate_not_null"))]
    pub author: DoubleOption<String>,
    #[serde(default, with = "::serde_with::rust::double_option")]
    pub publish_date: DoubleOption<DateTime<Utc>>,
}

pub fn validate_not_null<T>(field: &Option<Option<T>>) -> Result<(), ValidationError> {
    if let Some(None) = field {
        return Err(ValidationError::new("Field cannot be null"));
    }

    Ok(())
}
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章