Валидация данных в прикладных системах на JavaScript редко сводится к простому «разрешить или запретить». Почти всегда требуется одновременно защищать систему от некорректного ввода и не ломать удобство интеграции с внешними клиентами. Библиотека Class-validator занимает промежуточную позицию между жёсткой схемной валидацией и полностью динамическим подходом, предоставляя инструменты для точной настройки уровня строгости на уровне каждого поля.
Баланс между строгостью и гибкостью проявляется на нескольких уровнях: структура DTO, поведение отдельных декораторов, правила обработки отсутствующих значений, а также стратегия эволюции API.
Строгая валидация рассматривает входные данные как фиксированный контракт. Любое отклонение от него считается ошибкой.
Ключевые инструменты Class-validator для строгого подхода:
@IsDefined() — поле обязательно должно
присутствовать@IsNotEmpty() — значение не может быть пустым@IsString(), @IsNumber(),
@IsBoolean() — строгая типизация@ValidateNested() — строгая проверка вложенных
объектовПример строгого DTO:
import { IsDefined, IsString, IsInt, Min, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';
class CreateUserDto {
@IsDefined()
@IsString()
name: string;
@IsDefined()
@IsInt()
@Min(0)
age: number;
@IsDefined()
@ValidateNested()
@Type(() => AddressDto)
address: AddressDto;
}
class AddressDto {
@IsDefined()
@IsString()
city: string;
}
Такой подход гарантирует, что объект всегда соответствует ожидаемой структуре. Он полезен в доменных операциях, где отсутствие поля означает логическую ошибку.
В реальных API данные часто приходят частично: формы редактирования, PATCH-запросы, интеграции с внешними сервисами. Здесь строгая модель становится ограничением.
Гибкость в Class-validator достигается через:
@IsOptional() — разрешает отсутствие поля@ValidateIf() — условная валидацияПример гибкого DTO:
import { IsOptional, IsString, IsInt, Min } from 'class-validator';
class UpdateUserDto {
@IsOptional()
@IsString()
name?: string;
@IsOptional()
@IsInt()
@Min(0)
age?: number;
}
Такой подход позволяет отправлять только изменяемые поля, не требуя полного объекта.
@IsOptional() не просто пропускает поле — он влияет на
весь процесс валидации. Если значение undefined или
null, цепочка валидаторов для этого поля не
выполняется.
Это создаёт важный нюанс: поле становится не «необязательным», а «условно проверяемым».
Типичные ошибки:
@IsOptional() допускает пустую строку
(это не так)null и undefined без явной
политикиДля более строгого контроля часто комбинируют:
@IsOptional()
@IsString()
@IsNotEmpty()
name?: string;
Такой набор означает: поле может отсутствовать, но если присутствует — не может быть пустым.
@ValidateIf() позволяет строить зависимые правила, где
наличие или корректность одного поля влияет на другое.
import { ValidateIf, IsString } from 'class-validator';
class ProfileDto {
@IsString()
mode: 'simple' | 'advanced';
@ValidateIf(o => o.mode === 'advanced')
@IsString()
advancedConfig: string;
}
Здесь гибкость достигается не через ослабление правил, а через их контекстуализацию.
Это особенно важно для:
Разделение DTO по типу операции — один из ключевых способов управления балансом.
class ReplaceUserDto {
@IsString()
name: string;
@IsInt()
age: number;
}
Строгая модель: все поля обязательны.
class PatchUserDto {
@IsOptional()
@IsString()
name?: string;
@IsOptional()
@IsInt()
age?: number;
}
Гибкая модель: любое поле может быть пропущено.
Разделение DTO позволяет не усложнять одну модель множеством условных правил.
Class-validator поддерживает группы, позволяющие применять разные правила в зависимости от контекста.
import { IsString } from 'class-validator';
class UserDto {
@IsString({ groups: ['create'] })
name: string;
@IsString({ groups: ['update'] })
id: string;
}
При вызове валидации можно выбирать набор правил:
createupdateadminpublicГруппы позволяют избегать дублирования DTO, но увеличивают сложность понимания модели.
Баланс между строгостью и гибкостью касается не только значений, но и структуры объекта.
Настройки:
whitelist: true — удаляет лишние поляforbidNonWhitelisted: true — выбрасывает ошибку при
лишних поляхПример:
import { validate } from 'class-validator';
validate(dto, {
whitelist: true,
forbidNonWhitelisted: true,
});
Разница:
В API с внешними клиентами чаще используют whitelist, чтобы не ломать интеграции при расширении схемы.
В реальных приложениях данные часто приходят в «сыром» виде. Здесь
важна связка class-transformer и Class-validator.
import { plainToInstance } from 'class-transformer';
const dto = plainToInstance(UpdateUserDto, body);
Дополнительные настройки:
skipMissingPropertiesenableImplicitConversionЭти параметры влияют на степень строгости:
Вложенные DTO часто усиливают эффект строгой модели.
class CompanyDto {
@ValidateNested()
@Type(() => AddressDto)
address: AddressDto;
}
Если не настроить гибкость правильно:
@ValidateNested() требует существования объекта@IsOptional() приводит к обязательности
всего дереваГибкий вариант:
@IsOptional()
@ValidateNested()
@Type(() => AddressDto)
address?: AddressDto;
Здесь баланс достигается на уровне вложенности.
Когда стандартных декораторов недостаточно, создаются кастомные правила.
import { ValidatorConstraint, ValidatorConstraintInterface } from 'class-validator';
@ValidatorConstraint({ name: 'isEven', async: false })
class IsEvenConstraint implements ValidatorConstraintInterface {
validate(value: number) {
return value % 2 === 0;
}
}
Кастомная валидация позволяет:
Но чрезмерное использование приводит к скрытой бизнес-логике вне сервисного слоя.
Слишком жёсткая модель приводит к:
Признаки:
@IsOptional() там, где логически допустимы
частичные данные@IsNotEmpty() без бизнес-смыслаЧрезмерная гибкость проявляется иначе:
Последствия:
Рабочий подход обычно строится вокруг разделения уровней строгости:
Структурное разделение позволяет избегать попытки одной моделью покрыть все сценарии.
Любая система валидации живёт в условиях изменения данных.
Поддержание баланса достигается через:
@IsOptional()Пример версионирования:
class UserV1Dto {
@IsString()
name: string;
}
class UserV2Dto {
@IsString()
name: string;
@IsOptional()
@IsString()
surname?: string;
}
Степень проверки часто зависит не от DTO, а от контекста:
Class-validator позволяет реализовать это через:
Баланс между строгими и гибкими правилами не является точкой на шкале. Это набор независимых решений, каждое из которых регулирует отдельный аспект: