Валидация проходит, хотя не должна

Поведение, при котором данные проходят валидацию несмотря на очевидное несоответствие правилам, почти всегда связано не с ошибкой декораторов, а с контекстом выполнения проверки и тем, в каком виде объект попадает в class-validator.

Валидация в этой библиотеке опирается на два ключевых условия: наличие корректно созданного экземпляра класса и фактическое присутствие метаданных декораторов, которые применяются к этому экземпляру. Нарушение любого из этих условий приводит к тому, что проверки либо пропускаются, либо не срабатывают вовсе.


Одна из самых частых причин — передача «сырых» объектов, пришедших, например, из HTTP-запроса.

class CreateUserDto {
  @IsEmail()
  email: string;

  @Length(8)
  password: string;
}

При проверке:

const dto = {
  email: "not-an-email",
  password: "123"
};

validate(dto);

валидация не даст ожидаемого результата, поскольку объект не содержит метаданных класса CreateUserDto. Декораторы работают только с экземплярами, созданными через new.

Корректный вариант требует преобразования:

const dto = plainToInstance(CreateUserDto, {
  email: "not-an-email",
  password: "123"
});

и только затем:

validate(dto);

Отсутствие этого шага приводит к ситуации, когда проверки формально вызываются, но не имеют к чему применяться.


Отсутствие преобразования через class-transformer

Библиотека class-validator часто используется вместе с class-transformer. Без него входные данные остаются обычными объектами JavaScript.

Критичный момент проявляется при использовании вложенных структур:

class Profile {
  @IsString()
  city: string;
}

class User {
  @ValidateNested()
  profile: Profile;
}

Без plainToInstance вложенный объект не превращается в Profile, и @ValidateNested() фактически не запускает внутреннюю проверку.


Неявное пропускание полей (skipMissingProperties)

Опция skipMissingProperties изменяет поведение валидации таким образом, что отсутствующие поля не приводят к ошибке.

validate(dto, { skipMissingProperties: true });

При этом поле может быть обязательным с точки зрения бизнес-логики:

class User {
  @IsNotEmpty()
  username: string;
}

Если username отсутствует, но включён skipMissingProperties, ошибка не возникнет. Это создаёт иллюзию «успешной» валидации при неполных данных.


Несоответствие типов и преобразований

TypeScript-типизация не влияет на runtime-валидацию. Особенно критично это проявляется с числами и булевыми значениями, приходящими из JSON.

class Product {
  @IsNumber()
  price: number;
}

При входных данных:

{ price: "100" }

валидация может пройти успешно, если включено преобразование типов:

enableImplicitConversion: true

В таком случае строка "100" автоматически преобразуется в число, и проверка @IsNumber() проходит.


validateIf и условное отключение проверок

Декоратор @ValidateIf позволяет полностью исключать поле из проверки при выполнении условия.

class User {
  @ValidateIf(o => o.isAdmin === true)
  @IsEmail()
  email: string;
}

Если условие возвращает false, остальные декораторы игнорируются. Это часто становится причиной ситуации, когда поле кажется «невалидным», но ошибка не появляется.


Проблемы с null и undefined

Различие между null и undefined влияет на поведение многих валидаторов.

class Example {
  @IsNotEmpty()
  value: string;
}
  • undefined может игнорироваться при включённых опциях пропуска
  • null часто требует отдельного декоратора @IsNotEmpty({ each: false }) или @IsDefined

Отсутствие явной проверки @IsDefined() приводит к тому, что поле считается допустимо отсутствующим.


Группы валидации (groups)

Использование групп позволяет включать и выключать правила в зависимости от сценария.

class User {
  @IsEmail({}, { groups: ["create"] })
  email: string;
}

Если при вызове валидации не указаны группы:

validate(dto, { groups: ["update"] });

правила для create не выполняются. Это создаёт ситуацию, когда часть ограничений «исчезает» в зависимости от контекста.


Пропущенная валидация вложенных объектов

Даже при наличии @ValidateNested() проверка не выполняется без указания типа вложенного объекта.

class User {
  @ValidateNested()
  profile: Profile;
}

Без @Type(() => Profile) из class-transformer вложенный объект остаётся обычным литералом:

class User {
  @ValidateNested()
  @Type(() => Profile)
  profile: Profile;
}

Отсутствие этого преобразования приводит к тому, что вложенная структура фактически игнорируется.


forbidNonWhitelisted и whitelist

Опции whitelist и forbidNonWhitelisted влияют не на «валидацию», а на очистку входного объекта.

validate(dto, {
  whitelist: true
});

При включённом whitelist лишние поля удаляются, но не вызывают ошибку. Это часто воспринимается как «валидация прошла», хотя фактически данные были изменены.

Для строгого поведения требуется:

validate(dto, {
  whitelist: true,
  forbidNonWhitelisted: true
});

Проверка только существующих свойств объекта

class-validator не добавляет поля автоматически. Если свойство не существует в объекте, декораторы не активируются.

class User {
  @IsInt()
  age: number;
}

При объекте:

{}

без @IsDefined() ошибка может не появиться в зависимости от конфигурации, поскольку валидатор не всегда считает отсутствие свойства нарушением.


Скрытое влияние transformOptions

При использовании глобальных пайпов (например, в NestJS) часто включается:

transform: true

или:

transformOptions: {
  enableImplicitConversion: true
}

Это изменяет входные данные до валидации. В результате:

  • строки превращаются в числа
  • отсутствующие поля становятся undefined
  • лишние поля удаляются

Фактический объект, который проходит валидацию, уже отличается от исходного запроса, что приводит к неожиданному «успеху» проверок.


Итоговая природа проблемы

Ситуации, когда данные проходят валидацию вопреки ожиданиям, почти всегда связаны не с самими декораторами, а с одним из факторов:

  • отсутствие преобразования в экземпляр класса
  • неучтённые настройки validate()
  • влияние class-transformer
  • условные декораторы (groups, ValidateIf)
  • неявные преобразования типов
  • вложенные структуры без @Type и @ValidateNested

Поведение библиотеки строго соответствует полученному runtime-объекту, а не исходной структуре данных или TypeScript-типам.