В библиотеке class-validator основной механизм
расширения валидации строится вокруг пользовательских декораторов.
Встроенных ограничений достаточно для базовых сценариев (строки, числа,
диапазоны, форматы), однако реальные доменные модели часто требуют
специфических правил, которые невозможно выразить стандартными
средствами. Для таких случаев применяется создание кастомных декораторов
через registerDecorator.
Кастомный декоратор представляет собой функцию, которая регистрирует правило валидации и связывает его с классом и конкретным свойством. Внутри него описывается логика проверки, доступ к значению поля и возможность передачи параметров.
Основой любого пользовательского декоратора является функция, внутри
которой вызывается registerDecorator.
import {
registerDecorator,
ValidationOptions,
ValidationArguments,
} from 'class-validator';
export function IsPositiveEven(validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
registerDecorator({
name: 'isPositiveEven',
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
validator: {
validate(value: any, args: ValidationArguments) {
return typeof value === 'number' && value > 0 && value % 2 === 0;
},
},
});
};
}
В этой конструкции ключевыми элементами являются:
name — уникальное имя правила;target — класс, к которому привязан декоратор;propertyName — имя проверяемого свойства;options — параметры валидации (сообщения, группы,
условия);validator.validate — функция проверки значения.Минимальный кастомный декоратор может не содержать параметров и проверять одно условие.
export function IsEven(validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
registerDecorator({
name: 'isEven',
target: object.constructor,
propertyName,
options: validationOptions,
validator: {
validate(value: any) {
return typeof value === 'number' && value % 2 === 0;
},
},
});
};
}
Использование:
import { IsEven } from './validators';
class Product {
@IsEven({ message: 'Значение должно быть чётным числом' })
quantity: number;
}
ValidationArguments предоставляет доступ к контексту
валидации: объекту, имени свойства и другим метаданным. Это позволяет
создавать более гибкие проверки.
export function IsGreaterThan(property: string, validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
registerDecorator({
name: 'isGreaterThan',
target: object.constructor,
propertyName,
constraints: [property],
options: validationOptions,
validator: {
validate(value: any, args: ValidationArguments) {
const relatedPropertyName = args.constraints[0];
const relatedValue = (args.object as any)[relatedPropertyName];
return typeof value === 'number' &&
typeof relatedValue === 'number' &&
value > relatedValue;
},
},
});
};
}
Такой подход позволяет сравнивать значения разных полей внутри одного объекта.
class Range {
min: number;
@IsGreaterThan('min', {
message: 'max должно быть больше min',
})
max: number;
}
Кастомные декораторы могут принимать дополнительные параметры через
constraints. Это позволяет делать их универсальными.
export function MinLengthCustom(min: number, validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
registerDecorator({
name: 'minLengthCustom',
target: object.constructor,
propertyName,
constraints: [min],
options: validationOptions,
validator: {
validate(value: any, args: ValidationArguments) {
const [minLength] = args.constraints;
return typeof value === 'string' && value.length >= minLength;
},
},
});
};
}
Использование:
class User {
@MinLengthCustom(8, {
message: 'Пароль слишком короткий',
})
password: string;
}
class-validator поддерживает асинхронные проверки, например обращение
к базе данных или внешнему API. Для этого validate должен
возвращать Promise<boolean>.
export function IsEmailUnique(validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
registerDecorator({
name: 'isEmailUnique',
target: object.constructor,
propertyName,
options: validationOptions,
validator: {
async validate(value: any) {
const existingUser = await fakeDatabase.findUserByEmail(value);
return !existingUser;
},
},
});
};
}
Асинхронные декораторы особенно важны в доменных системах с уникальными ограничениями.
Внутри validate доступен полный объект, что позволяет
реализовывать сложные межполевые зависимости.
export function IsAfter(property: string, validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
registerDecorator({
name: 'isAfter',
target: object.constructor,
propertyName,
constraints: [property],
options: validationOptions,
validator: {
validate(value: any, args: ValidationArguments) {
const relatedProperty = args.constraints[0];
const relatedValue = (args.object as any)[relatedProperty];
if (!value || !relatedValue) return false;
return new Date(value) > new Date(relatedValue);
},
},
});
};
}
Пример применения:
class Event {
startDate: string;
@IsAfter('startDate', {
message: 'Дата окончания должна быть позже даты начала',
})
endDate: string;
}
Кастомные декораторы можно комбинировать с встроенными ограничениями class-validator, создавая многоуровневую валидацию.
import { IsString, IsNotEmpty } from 'class-validator';
class Account {
@IsString()
@IsNotEmpty()
@MinLengthCustom(5)
username: string;
}
Такой подход позволяет разделять ответственность: базовые проверки остаются декларативными, а доменная логика выносится в отдельные декораторы.
ValidationOptions управляет поведением ошибки и
группировкой правил.
registerDecorator({
name: 'isEven',
target: object.constructor,
propertyName,
options: {
message: 'Число должно быть чётным',
groups: ['numeric'],
...validationOptions,
},
validator: {
validate(value: any) {
return typeof value === 'number' && value % 2 === 0;
},
},
});
Основные возможности:
message — текст ошибки;groups — группировка правил;always — постоянная проверка независимо от
условий;context — пользовательские данные для обработчиков
ошибок.Одной из частых проблем является потеря контекста this.
Внутри validate нельзя полагаться на this,
поскольку функция вызывается вне класса.
validator: {
validate(value: any) {
return this.check(value); // некорректно
},
}
Корректный подход — использование
ValidationArguments.
Ещё одна ошибка — неправильная работа с асинхронностью. Если функция
возвращает Promise, но не объявлена как async,
поведение становится непредсказуемым.
Кастомный декоратор может включать сложную бизнес-логику, объединяя несколько проверок.
export function IsValidDiscount(validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
registerDecorator({
name: 'isValidDiscount',
target: object.constructor,
propertyName,
options: validationOptions,
validator: {
validate(value: any) {
return (
typeof value === 'number' &&
value >= 0 &&
value <= 100 &&
Number.isInteger(value)
);
},
},
});
};
}
При проектировании моделей часто требуется проверка согласованности данных.
export function IsNotSameAs(property: string, validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
registerDecorator({
name: 'isNotSameAs',
target: object.constructor,
propertyName,
constraints: [property],
options: validationOptions,
validator: {
validate(value: any, args: ValidationArguments) {
const related = (args.object as any)[args.constraints[0]];
return value !== related;
},
},
});
};
}
При росте проекта декораторы обычно выносятся в отдельный слой:
validators/
string.validators.tsnumber.validators.tsdate.validators.tsbusiness.validators.tsТакое разделение снижает связность и упрощает повторное использование в разных модулях.
Типичный декоратор включает:
registerDecorator;validator с validate;ValidationArguments при необходимости;ValidationOptions.Эта модель делает систему валидации расширяемой, позволяя интегрировать любые правила без изменения ядра библиотеки.