Во многих системах стандартных декораторов недостаточно для описания требований к паролю. Часто требуется проверка сразу нескольких условий: длина, наличие цифр, символов верхнего и нижнего регистра, специальных символов и отсутствие пробелов.
Реализация кастомного валидатора через
ValidatorConstraint позволяет вынести такую логику в
переиспользуемую сущность.
import {
ValidatorConstraint,
ValidatorConstraintInterface,
ValidationArguments,
registerDecorator
} from 'class-validator';
@ValidatorConstraint({ name: 'IsStrongPassword', async: false })
export class IsStrongPasswordConstraint implements ValidatorConstraintInterface {
validate(value, args) {
if (typeof value !== 'string') return false;
const lengthOk = value.length >= 8;
const hasUpperCase = /[A-Z]/.test(value);
const hasLowerCase = /[a-z]/.test(value);
const hasNumber = /\d/.test(value);
const hasSpecial = /[!@#$%^&*(),.?":{}|<>]/.test(value);
const hasSpaces = /\s/.test(value);
return lengthOk && hasUpperCase && hasLowerCase && hasNumber && hasSpecial && !hasSpaces;
}
defaultMessage(args) {
return 'Пароль не соответствует требованиям сложности';
}
}
export function IsStrongPassword(validationOptions) {
return function (object, propertyName) {
registerDecorator({
target: object.constructor,
propertyName,
options: validationOptions,
constraints: [],
validator: IsStrongPasswordConstraint,
});
};
}
Использование:
import { IsStrongPassword } from './validators/password.validator';
class UserDto {
@IsStrongPassword()
password;
}
Реальные приложения требуют проверки уникальности email или логина. Такая логика всегда асинхронна и опирается на внешний источник данных.
import {
ValidatorConstraint,
ValidatorConstraintInterface,
registerDecorator
} from 'class-validator';
@ValidatorConstraint({ async: true })
export class IsEmailUniqueConstraint implements ValidatorConstraintInterface {
constructor(userRepository) {
this.userRepository = userRepository;
}
async validate(email) {
if (typeof email !== 'string') return false;
const user = await this.userRepository.findOne({ email });
return !user;
}
defaultMessage() {
return 'Email уже используется';
}
}
export function IsEmailUnique(validationOptions) {
return function (object, propertyName) {
registerDecorator({
target: object.constructor,
propertyName,
options: validationOptions,
constraints: [],
validator: IsEmailUniqueConstraint,
});
};
}
Использование в DTO:
class RegisterDto {
@IsEmailUnique()
email;
}
Распространённый сценарий — проверка совпадения пароля и подтверждения пароля.
import {
ValidatorConstraint,
ValidatorConstraintInterface,
ValidationArguments,
registerDecorator
} from 'class-validator';
@ValidatorConstraint({ name: 'MatchFields', async: false })
export class MatchFieldsConstraint implements ValidatorConstraintInterface {
validate(value, args) {
const [relatedPropertyName] = args.constraints;
const relatedValue = args.object[relatedPropertyName];
return value === relatedValue;
}
defaultMessage(args) {
const [relatedPropertyName] = args.constraints;
return `${args.property} должно совпадать с ${relatedPropertyName}`;
}
}
export function Match(property, validationOptions) {
return function (object, propertyName) {
registerDecorator({
target: object.constructor,
propertyName,
constraints: [property],
options: validationOptions,
validator: MatchFieldsConstraint,
});
};
}
Использование:
class ChangePasswordDto {
password;
@Match('password')
confirmPassword;
}
В реальных системах часто требуется ограничение даты, например: дата бронирования не может быть в прошлом и не может превышать определённый горизонт.
import {
ValidatorConstraint,
ValidatorConstraintInterface,
registerDecorator
} from 'class-validator';
@ValidatorConstraint({ async: false })
export class IsValidBookingDateConstraint implements ValidatorConstraintInterface {
validate(value) {
if (!(value instanceof Date)) return false;
const now = new Date();
const maxDate = new Date();
maxDate.setMonth(maxDate.getMonth() + 6);
return value >= now && value <= maxDate;
}
defaultMessage() {
return 'Дата бронирования должна быть в пределах 6 месяцев от текущей даты';
}
}
export function IsValidBookingDate(validationOptions) {
return function (object, propertyName) {
registerDecorator({
target: object.constructor,
propertyName,
options: validationOptions,
validator: IsValidBookingDateConstraint,
});
};
}
Сложные DTO часто содержат зависимые поля: например, поле
companyName обязательно только если isCompany
установлено в true.
import {
ValidatorConstraint,
ValidatorConstraintInterface,
registerDecorator
} from 'class-validator';
@ValidatorConstraint({ async: false })
export class IsRequiredIfConstraint implements ValidatorConstraintInterface {
validate(value, args) {
const [dependentProperty, expectedValue] = args.constraints;
const object = args.object;
if (object[dependentProperty] === expectedValue) {
return value !== undefined && value !== null && value !== '';
}
return true;
}
defaultMessage(args) {
const [dependentProperty, expectedValue] = args.constraints;
return `${args.property} обязательно, когда ${dependentProperty} равно ${expectedValue}`;
}
}
export function IsRequiredIf(property, value, validationOptions) {
return function (object, propertyName) {
registerDecorator({
target: object.constructor,
propertyName,
constraints: [property, value],
options: validationOptions,
validator: IsRequiredIfConstraint,
});
};
}
Пример применения:
class CompanyDto {
isCompany;
@IsRequiredIf('isCompany', true)
companyName;
}
Валидация массивов сложных объектов часто выходит за рамки стандартных декораторов.
Пример: проверка, что каждый элемент массива имеет уникальный идентификатор.
import {
ValidatorConstraint,
ValidatorConstraintInterface,
registerDecorator
} from 'class-validator';
@ValidatorConstraint({ async: false })
export class IsUniqueArrayFieldConstraint implements ValidatorConstraintInterface {
validate(array) {
if (!Array.isArray(array)) return false;
const ids = array.map(item => item.id);
const uniqueIds = new Set(ids);
return ids.length === uniqueIds.size;
}
defaultMessage() {
return 'Элементы массива должны иметь уникальные id';
}
}
export function IsUniqueArrayField(validationOptions) {
return function (object, propertyName) {
registerDecorator({
target: object.constructor,
propertyName,
options: validationOptions,
validator: IsUniqueArrayFieldConstraint,
});
};
}
В реальных системах часто используются строгие форматы артикулов или идентификаторов.
import {
ValidatorConstraint,
ValidatorConstraintInterface,
registerDecorator
} from 'class-validator';
@ValidatorConstraint({ async: false })
export class IsSkuFormatConstraint implements ValidatorConstraintInterface {
validate(value) {
if (typeof value !== 'string') return false;
return /^[A-Z]{3}-\d{4}-[A-Z0-9]{2}$/.test(value);
}
defaultMessage() {
return 'SKU должен соответствовать формату XXX-0000-XX';
}
}
export function IsSkuFormat(validationOptions) {
return function (object, propertyName) {
registerDecorator({
target: object.constructor,
propertyName,
options: validationOptions,
validator: IsSkuFormatConstraint,
});
};
}
Кастомные валидаторы в class-validator становятся частью
доменной логики, а не просто инструментом проверки входных данных. Их
использование особенно эффективно при следующих условиях:
Такая организация позволяет удерживать валидационную логику рядом с моделью данных, не перегружая сервисный слой лишними проверками и условиями.