I'm trying to build a validator/decorator to validate one DTO attribute based on another DTO attribute value. I need to validate if availability.saleDate
is before details.date
.
The problem I'm having is that the decorator at the top level in ItemDto isn't called. Is there a way to achieve this cross-DTO validation using class-validator? Should I be validating this differently? Aný thoughts?
Validator code:
@ValidatorConstraint({ name: 'IsSaleBeforeEventDate', async: false })
export class IsSaleBeforeEventDateConstraint
implements ValidatorConstraintInterface
{
validate(_: any, args: ValidationArguments) {
const item = args.object as any;
const saleDate = item.availability?.saleDateTime;
const eventDate = item.details?.date;
if (saleDate == null || eventDate == null) {
return true; // let other decorators handle required-ness
}
return new Date(saleDate) < new Date(eventDate);
}
defaultMessage(args: ValidationArguments) {
return `availability.saleDateTime must be before details.date`;
}
}
export function IsSaleBeforeEventDate(validationOptions?: ValidationOptions) {
return function (object: object) {
registerDecorator({
name: 'IsSaleBeforeEventDate',
target: object.constructor,
propertyName: '',
constraints: [],
options: validationOptions,
validator: IsSaleBeforeEventDateConstraint,
});
};
}
Usage in the DTO:
@IsSaleBeforeEventDate()
export class ItemDto {
readonly details?: DetailsTabDto;
readonly availability?: AvailabilityTabDto;
}
export class DetailsTabDto {
readonly date?: string;
}
export class AvailabilityTabDto {
readonly saleDateTime?: string;
}
To solve your cross-DTO validation issue, you need to work around class-validator's limitation with class-level decorators (see GitHub Issue #182).
Here's a working solution using a getter method with the @Validate
decorator:
import { Validate, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';
export class ItemDto {
@ValidateNested()
@Type(() => DetailsTabDto)
readonly details?: DetailsTabDto;
@ValidateNested()
@Type(() => AvailabilityTabDto)
readonly availability?: AvailabilityTabDto;
@Validate(IsSaleBeforeEventDateConstraint)
get validateDates() {
return {
saleDate: this.availability?.saleDateTime,
eventDate: this.details?.date
};
}
}
This works because:
In your IsSaleBeforeEventDateConstraint
, update the validate method:
validate(value: any, args: ValidationArguments) {
const { saleDate, eventDate } = value;
if (saleDate == null || eventDate == null) {
return true; // let other decorators handle required-ness
}
return new Date(saleDate) < new Date(eventDate);
}
Don't forget to enable transformation in your NestJS ValidationPipe:
app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: true,
}),
);
This approach keeps your validation logic clean and contained within the DTO itself.