Валидация с зависимостями между полями в class-validator
основана на способности учитывать состояние всего объекта при проверке
отдельного свойства. Это позволяет реализовывать сценарии, где
корректность одного поля определяется значением другого, а также строить
комплексные правила, выходящие за рамки изолированных проверок.
Основной особенностью подобных сценариев является переход от декларативных одиночных декораторов к контекстным проверкам, использующим объект целиком через функции валидаторов или пользовательские constraint-классы.
@ValidateIfДекоратор @ValidateIf управляет тем, будет ли
выполняться последующая валидация свойства. Условие получает доступ к
объекту целиком, что позволяет связывать поля между собой.
Типичный механизм работы:
true или falsefalse остальные валидаторы свойства не
выполняютсяimport { ValidateIf, IsNotEmpty, IsEmail } from 'class-validator';
class UserDto {
role: string;
@ValidateIf(o => o.role === 'admin')
@IsEmail()
email: string;
}
В данном примере поле email становится обязательным и
проверяемым только при значении role = 'admin'.
Такой подход часто используется для:
Когда требуется сравнение значений двух свойств, встроенных декораторов становится недостаточно. Основной инструмент — создание собственного constraint.
Механизм состоит из двух частей:
ValidatorConstraintInterfaceimport {
ValidatorConstraint,
ValidatorConstraintInterface,
ValidationArguments,
} from 'class-validator';
@ValidatorConstraint({ name: 'Match', async: false })
export class MatchConstraint implements ValidatorConstraintInterface {
validate(value: any, args: ValidationArguments) {
const [relatedPropertyName] = args.constraints;
const relatedValue = (args.object as any)[relatedPropertyName];
return value === relatedValue;
}
defaultMessage(args: ValidationArguments) {
const [relatedPropertyName] = args.constraints;
return `Значение должно совпадать с ${relatedPropertyName}`;
}
}
Декоратор-обёртка:
import { registerDecorator, ValidationOptions } from 'class-validator';
import { MatchConstraint } from './match.constraint';
export function Match(property: string, options?: ValidationOptions) {
return function (object: any, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName,
options,
constraints: [property],
validator: MatchConstraint,
});
};
}
Наиболее распространённый кейс зависимой валидации — проверка совпадения пароля и его подтверждения.
import { IsString, MinLength } from 'class-validator';
import { Match } from './match.decorator';
class RegisterDto {
@IsString()
@MinLength(8)
password: string;
@IsString()
@Match('password', { message: 'Пароли не совпадают' })
confirmPassword: string;
}
Здесь значение confirmPassword сравнивается с
password через доступ к объекту в
ValidationArguments.
Зависимости между временными полями требуют сравнения значений одного типа.
Пример constraint для проверки, что конечная дата не меньше начальной:
@ValidatorConstraint({ name: 'IsAfter', async: false })
export class IsAfterConstraint implements ValidatorConstraintInterface {
validate(value: any, args: ValidationArguments) {
const [relatedPropertyName] = args.constraints;
const relatedValue = (args.object as any)[relatedPropertyName];
if (!value || !relatedValue) return true;
const start = new Date(relatedValue);
const end = new Date(value);
return end >= start;
}
defaultMessage() {
return 'Конечная дата должна быть больше или равна начальной';
}
}
Использование:
class PeriodDto {
startDate: string;
@Validate(IsAfterConstraint, ['startDate'])
endDate: string;
}
Зависимости могут включать более сложные условия, чем простое сравнение. Например, проверка обязательности поля в зависимости от комбинации значений.
import { ValidateIf, IsString } from 'class-validator';
class AccessDto {
type: string;
@ValidateIf(o => o.type === 'token' || o.type === 'apikey')
@IsString()
credentials: string;
}
Здесь поле credentials становится обязательным при
нескольких возможных значениях type.
Сценарий, при котором требуется заполнение хотя бы одного из нескольких полей, реализуется через кастомный валидатор, работающий на уровне объекта.
@ValidatorConstraint({ name: 'AtLeastOne', async: false })
export class AtLeastOneConstraint implements ValidatorConstraintInterface {
validate(_: any, args: ValidationArguments) {
const fields = args.constraints;
const object = args.object as any;
return fields.some(field => {
const value = object[field];
return value !== null && value !== undefined && value !== '';
});
}
defaultMessage(args: ValidationArguments) {
return `Необходимо заполнить хотя бы одно из полей: ${args.constraints.join(', ')}`;
}
}
Использование:
class ContactDto {
email?: string;
phone?: string;
@Validate(AtLeastOneConstraint, ['email', 'phone'])
_check: string;
}
Здесь служебное поле используется исключительно как точка привязки валидатора.
При работе с вложенными структурами зависимые проверки часто распространяются на вложенные DTO.
import { ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';
class AddressDto {
country: string;
city: string;
}
class UserProfileDto {
@ValidateNested()
@Type(() => AddressDto)
address: AddressDto;
role: string;
}
Сложные зависимости могут учитываться внутри вложенного объекта через передачу контекста родительского объекта в кастомные валидаторы.
ValidationArguments.object всегда содержит корневой
объект, даже при вложенной валидации. Это позволяет реализовывать
межуровневые зависимости:
@ValidatorConstraint({ name: 'CityDependsOnCountry', async: false })
export class CityDependsOnCountry implements ValidatorConstraintInterface {
validate(value: any, args: ValidationArguments) {
const object = args.object as any;
if (object.address?.country === 'Kazakhstan') {
return value === 'Karaganda' || value === 'Almaty';
}
return true;
}
}
@ValidateIf и кастомных constraintСложные правила часто строятся через комбинацию подходов:
@ValidateIf отключает ненужные проверкиclass PaymentDto {
method: string;
@ValidateIf(o => o.method === 'card')
@IsString()
cardNumber: string;
@ValidateIf(o => o.method === 'card')
@IsString()
expiryDate: string;
}
async: true
в ValidatorConstraintПри проектировании зависимой валидации важно учитывать, что:
@ValidateIf усложняет
трассировку правилКогда требуется объединение нескольких зависимостей, валидаторы могут применяться одновременно к одному полю или набору полей, формируя композицию ограничений:
class TaskDto {
status: string;
priority: number;
@ValidateIf(o => o.status === 'open')
@Validate(IsAfterConstraint, ['createdAt'])
dueDate: string;
}
Такой подход позволяет строить декларативные модели данных, в которых правила валидации становятся частью структуры DTO, а не внешней логики.