如何实现DTO中周期价格属性的可选性与合法性验证?
问题:NestJS DTO验证规则不符合预期
现有如下DTO定义:
export class UpsertSubscription { @Expose() @ValidateIf((o) => !o.monthly && !o.yearly) @IsNumber({}, { message: 'tiers.priceMustBeANumber' }) @Min(0, { message: 'tiers.priceMustBeAtLeastZero' }) public daily?: number; @Expose() @ValidateIf((o) => !o.daily && !o.yearly) @IsNumber({}, { message: 'tiers.priceMustBeANumber' }) @Min(0, { message: 'tiers.priceMustBeAtLeastZero' }) public monthly?: number; @Expose() @ValidateIf((o) => !o.daily && !o.monthly) @IsNumber({}, { message: 'tiers.priceMustBeANumber' }) @Min(0, { message: 'tiers.priceMustBeAtLeastZero' }) public yearly?: number; }
原本期望通过ValidateIf实现:当另外两个属性中的任意一个已定义时,当前属性为可选;但实际情况是,比如设置daily=10时,monthly可以传入任意非法值(如"something different"),验证不会触发,不符合预期。需求是:当其中一个属性已定义时,其余属性为可选,但如果设置了就必须符合验证规则。
解决方案
问题根源
原来的ValidateIf逻辑是仅当另外两个属性都未定义时,才执行当前属性的验证。这就导致只要其他属性存在,当前属性的验证会被直接跳过,非法值也能通过校验,和预期完全相反。
修正属性级验证
调整每个属性的ValidateIf条件,改为只要当前属性被设置(有值),就执行验证;未设置则跳过(保持可选):
export class UpsertSubscription { @Expose() @ValidateIf((o) => o.daily !== undefined) @IsNumber({}, { message: 'tiers.priceMustBeANumber' }) @Min(0, { message: 'tiers.priceMustBeAtLeastZero' }) public daily?: number; @Expose() @ValidateIf((o) => o.monthly !== undefined) @IsNumber({}, { message: 'tiers.priceMustBeANumber' }) @Min(0, { message: 'tiers.priceMustBeAtLeastZero' }) public monthly?: number; @Expose() @ValidateIf((o) => o.yearly !== undefined) @IsNumber({}, { message: 'tiers.priceMustBeANumber' }) @Min(0, { message: 'tiers.priceMustBeAtLeastZero' }) public yearly?: number; }
这样修改后,无论其他属性是否存在,只要当前属性被赋值,就会触发IsNumber和Min验证,非法值会被拦截。
可选:强制至少一个属性必填
如果需要确保请求中至少传入一个价格属性(daily/monthly/yearly中的一个),可以添加类级别的自定义验证:
- 定义验证约束:
import { ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments } from 'class-validator'; @ValidatorConstraint({ name: 'atLeastOnePrice', async: false }) export class AtLeastOnePrice implements ValidatorConstraintInterface { validate(obj: UpsertSubscription) { return obj.daily !== undefined || obj.monthly !== undefined || obj.yearly !== undefined; } defaultMessage() { return 'tiers.atLeastOnePriceRequired'; } }
- 在DTO类上添加验证装饰器:
import { Validate } from 'class-validator'; @Validate(AtLeastOnePrice) export class UpsertSubscription { // ... 原有属性定义 }
内容的提问来源于stack exchange,提问作者Omar Bahutair
相关产品推荐
相关产品推荐

