При построении структурированных приложений на JavaScript и TypeScript классы часто выступают в роли контейнеров данных, описывающих доменные сущности: пользователь, заказ, продукт, транзакция. В такой модели входящие данные приводятся к экземплярам классов, после чего применяется проверка корректности структуры и значений.
Библиотека class-validator опирается на декларативный
подход: правила описываются через декораторы, которые прикрепляются к
свойствам или к самому классу. Это позволяет отделить валидационную
логику от бизнес-кода и сделать правила переиспользуемыми и
прозрачными.
Валидация на уровне класса используется в случаях, когда проверка зависит не от одного поля, а от состояния объекта целиком: согласованность нескольких свойств, взаимные ограничения, вычисляемые условия.
В основе библиотеки лежит система метаданных. Каждый декоратор записывает информацию о правилах проверки в реестр, связанный с целевым классом. При вызове функции валидации эта метаинформация извлекается и применяется к экземпляру объекта.
Основные элементы архитектуры:
Валидация запускается через функции:
validate(instance)validateOrReject(instance)validateSync(instance)Каждая из них анализирует объект и возвращает список ошибок или выбрасывает исключение.
Валидация свойств применяется к отдельным полям:
import { IsString, IsEmail } from "class-validator";
class User {
@IsString()
name;
@IsEmail()
email;
}
Такая модель подходит для локальных ограничений.
Однако существуют сценарии, где изолированная проверка полей недостаточна. Например:
В этих случаях применяется класс-уровневая валидация.
Для реализации логики на уровне объекта используется
ValidatorConstraint.
Базовая структура:
import {
ValidatorConstraint,
ValidatorConstraintInterface,
} from "class-validator";
Создание ограничения:
@ValidatorConstraint({ name: "matchPasswords", async: false })
class MatchPasswordsConstraint {
validate(object) {
return object.password === object.confirmPassword;
}
defaultMessage() {
return "Пароли не совпадают";
}
}
Применение через декоратор:
import { Validate } from "class-validator";
class RegisterUser {
password;
confirmPassword;
@Validate(MatchPasswordsConstraint)
static passwordCheck;
}
В данном случае логика работает на уровне всего объекта, а не отдельных свойств.
Более гибкая проверка возможна через аргументы валидатора:
class MatchFieldsConstraint {
validate(value, args) {
const object = args.object;
return object.password === object.confirmPassword;
}
defaultMessage() {
return "Поля не совпадают";
}
}
ValidationArguments предоставляет:
object — текущий экземпляр классаvalue — значение текущего поля (если применимо)constraints — дополнительные параметры декоратораproperty — имя свойстваЭто позволяет реализовывать сложные межполевые зависимости.
Вместо использования класса напрямую создаётся кастомный декоратор:
import {
registerDecorator,
ValidationOptions,
} from "class-validator";
function Match(property, options) {
return function (object, propertyName) {
registerDecorator({
target: object.constructor,
propertyName,
options,
constraints: [property],
validator: {
validate(value, args) {
const [relatedProperty] = args.constraints;
const relatedValue = args.object[relatedProperty];
return value === relatedValue;
},
},
});
};
}
Использование:
class User {
password;
@Match("password")
confirmPassword;
}
Такой подход переносит валидацию ближе к декларативной модели, сохраняя при этом логику на уровне класса.
Класс-уровневая валидация часто применяется для обеспечения целостности состояния объекта.
@ValidatorConstraint({ name: "dateRange" })
class DateRangeConstraint {
validate(_, args) {
const obj = args.object;
return new Date(obj.startDate) < new Date(obj.endDate);
}
}
class Event {
startDate;
endDate;
}
Такая проверка невозможна через одиночные декораторы без потери выразительности.
Некоторые проверки требуют внешних данных: базы данных, API, кэша.
@ValidatorConstraint({ async: true })
class UniqueEmailConstraint {
async validate(email) {
const user = await database.findUserByEmail(email);
return !user;
}
defaultMessage() {
return "Email уже существует";
}
}
Асинхронная логика полностью поддерживается механизмом
class-validator, включая цепочку промисов при вызове
validate().
Класс-уровневая логика часто комбинируется с вложенными структурами:
import { ValidateNested } from "class-validator";
import { Type } from "class-transformer";
class Address {
city;
street;
}
class User {
name;
@ValidateNested()
@Type(() => Address)
address;
}
В этом случае каждый вложенный объект проходит собственный цикл валидации, но итоговая структура оценивается как единое целое.
Группы позволяют активировать разные наборы ограничений в зависимости от сценария:
import { IsOptional } from "class-validator";
class User {
@IsOptional({ groups: ["update"] })
password;
}
Класс-уровневая логика может учитывать группы через
ValidationArguments, позволяя адаптировать правила под
разные режимы работы системы.
Результат валидации представляет собой массив объектов:
property — имя поляconstraints — список нарушенных правилchildren — вложенные ошибкиtarget — исходный объектКласс-уровневые ошибки могут быть привязаны к виртуальному свойству или к корневому объекту, формируя глобальное сообщение об ошибке.
Практическая модель почти всегда сочетает оба подхода:
class Payment {
amount;
currency;
@Validate(PaymentConsistencyConstraint)
static _;
}
Такой подход разделяет синтаксическую и семантическую валидацию, снижая связность логики.
Механизм class-validator проходит по метаданным один раз за цикл валидации, после чего последовательно выполняет все проверки.
Класс-уровневые ограничения могут быть более затратными, поскольку:
Оптимизация достигается через:
Класс-уровневая валидация используется в доменных моделях, где важна целостность состояния:
Такая модель позволяет описывать сложные условия без разрастания логики в сервисных слоях и контроллерах.