I'm having issues writing some nested validation in NestJS using class-validator and class-transformer packages.
import { Type } from 'class-transformer';
import { GradeValidator } from '../validation';
import { IsIn, Validate, ValidateNested } from 'class-validator';
export class CurrentGradeDto {
@Validate(GradeValidator)
grade: string;
}
export class OnboardDto {
@IsIn(['system1', 'system2', 'system3', 'system4'])
gradingSystem: string;
@ValidateNested({ each: true })
@Type(() => CurrentGradeDto)
currentGrades: CurrentGradeDto[];
}
I'm sending multiple grades inside the nested object currentGrades
and I need to validate each one against the property stored inside gradingSystem
of the parent class.
Is there any way to send gradingSystem
information to the GradeValidator
class?
The answer is close to what @HaiAlison proposed. The @Validation decorator must be present inside the parent class:
import { Type } from 'class-transformer';
import { GradeValidator } from '../validation';
import { IsIn, Validate, ValidateNested } from 'class-validator';
export class CurrentGradeDto {
grade: string;
}
export class OnboardDto {
@IsIn(['system1', 'system2', 'system3', 'system4'])
gradingSystem: string;
@ValidateNested({ each: true })
@Type(() => CurrentGradeDto)
@Validate(GradeValidator) <---- Here
currentGrades: CurrentGradeDto[];
}
Then the first parameter inside the validate method of GradeValidator is an array of grades and the second parameter (args) contains the grading system information:
export class CurrentGradeValidator implements ValidatorConstraintInterface
{
validate(grades: CurrentGradeDto[], args: ValidationArguments) {
const { gradingSystem } = args.object as OnboardDto;
for (let i = 0; i < grades.length; i++) {
const grade = grades[i].grade;
if () {
//check if current grade is in gradingSystem
}
}
}
}
Now if the validation fails it will trigger the defaultMessage method. The idea is now to iterate once again over the array to display all of the grades that failed the validation:
defaultMessage(args?: ValidationArguments): string {
let errMessages: string[] = [];
const { gradingSystem, currentGrades } = args.object as OnboardDto;
for (let i = 0; i < currentGrades.length; i++) {
const grade = currentGrades[i].grade as any;
if (!GradeUtil.gradeIsInSystem({ grade, gradingSystem })) {
errMessages.push(grade);
}
}
return `Invalid current grades ${errMessages.join(
',',
)} for the specified grading system ${gradingSystem}`;
}
Another cool trick that I found out is that you can define a private property inside the constructor of the class which will contain the error message depending on what condition failed:
@ValidatorConstraint({ name: 'gradeValidator', async: false })
export class GradeValidator implements ValidatorConstraintInterface {
constructor(private errMessage: string) {}
validate(payload: any, args: ValidationArguments) {
if (check 1) {
this.errMessage = `error message 1`;
return false;
}
if (check 2) {
this.errMessage = `error message 2`;
return false;
}
return true;
}
defaultMessage(args?: ValidationArguments): string {
return this.errMessage;
}
}
Using this approach you don't need to have a predefined error message.