В class-validator асинхронные правила часто «не срабатывают» не из-за самой библиотеки, а из-за особенностей её модели выполнения: асинхронная валидация существует отдельно от синхронного потока и требует явного корректного запуска через Promise-ориентированный API.
Основное отличие заключается в том, что библиотека не выполняет
автоматическое ожидание асинхронных ограничений внутри синхронных
функций. Любая асинхронная проверка должна быть частью цепочки, которая
возвращает Promise<ValidationError[]>, иначе
результат либо игнорируется, либо теряется в процессе выполнения.
Вся логика class-validator делится на два уровня:
validateSync()validate()Ключевой момент — асинхронные ограничения работают только при
использовании validate().
import { validate } from 'class-validator';
const errors = await validate(dto);
Если использовать:
import { validateSync } from 'class-validator';
const errors = validateSync(dto);
любые асинхронные правила будут полностью проигнорированы, потому что
validateSync физически не поддерживает Promise.
Наиболее частая ситуация — вызов validate() без ожидания
результата.
validate(dto); // ошибка: результат не используется
Фактически функция возвращает Promise, но дальнейшая логика продолжает выполняться до завершения проверок. Это создаёт ощущение, что асинхронная валидация «не работает».
Корректный вариант:
const errors = await validate(dto);
или:
validate(dto).then(errors => {
// обработка
});
Асинхронность в class-validator чаще всего реализуется через
ValidatorConstraint с async validate().
import {
ValidatorConstraint,
ValidatorConstraintInterface,
} from 'class-validator';
@ValidatorConstraint({ async: true })
export class IsEmailUniqueConstraint implements ValidatorConstraintInterface {
async validate(email: string): Promise<boolean> {
const user = await database.users.findByEmail(email);
return !user;
}
defaultMessage(): string {
return 'Email уже используется';
}
}
Критические условия, без которых асинхронная логика не выполнится:
async: true в декораторе
ValidatorConstraintvalidate возвращает
Promise<boolean>Если убрать async: true, поведение становится
синхронным:
@ValidatorConstraint()
export class BrokenConstraint {
async validate(value: string) {
return await checkSomething(value);
}
}
В этом случае class-validator может интерпретировать Promise как truthy-значение, что приводит к некорректному результату: проверка всегда проходит.
Асинхронная функция обязана возвращать результат явно. Частая ошибка — выполнение запроса без return:
async validate(email: string): Promise<boolean> {
database.users.findByEmail(email); // нет return
}
Фактически возвращается undefined, что интерпретируется
как false или приводит к неожиданному поведению в
зависимости от контекста.
Правильно:
async validate(email: string): Promise<boolean> {
const user = await database.users.findByEmail(email);
return !user;
}
В экосистеме NestJS часто возникает ситуация, когда асинхронная валидация не выполняется из-за пайпов.
Если используется ValidationPipe:
app.useGlobalPipes(new ValidationPipe());
и при этом отключён transform или неправильно настроен pipe, результат может быть обработан до завершения асинхронных проверок.
Особенно проблемные случаи:
await validate()class-transformer без корректного
преобразования типовРаспространённый анти-паттерн:
function createUser(dto) {
const errors = validate(dto);
if (errors.length) {
throw new Error('validation failed');
}
return repository.save(dto);
}
Здесь errors — Promise, а не массив. Условие всегда
истинно или всегда ложное в зависимости от приведения типов.
Корректный вариант:
async function createUser(dto) {
const errors = await validate(dto);
if (errors.length) {
throw new Error('validation failed');
}
return repository.save(dto);
}
Асинхронные правила внутри вложенных DTO требуют включения рекурсивной валидации:
import { Type } from 'class-transformer';
import { ValidateNested } from 'class-validator';
class ProfileDto {
@ValidateNested()
@Type(() => UserDto)
user: UserDto;
}
Если @ValidateNested() отсутствует, вложенные
асинхронные проверки не выполняются вовсе, создавая эффект «частичной
валидации».
Асинхронная валидация в class-validator выполняется последовательно в рамках одной цепочки Promise. Это означает:
При большом количестве async constraints это может создавать задержки, которые ошибочно воспринимаются как «валидация зависла».
Некорректные типы возврата часто ломают асинхронную логику:
async validate(value: string) {
if (!value) return; // undefined
}
Class-validator ожидает строго boolean или
Promise<boolean>. Любое отклонение интерпретируется
как ошибка логики валидации.
Правильно:
async validate(value: string): Promise<boolean> {
if (!value) return false;
return true;
}
Если на одном поле смешиваются синхронные и асинхронные правила, порядок выполнения может создавать иллюзию отсутствия async-валидации.
@IsNotEmpty()
@Validate(IsEmailUniqueConstraint)
email: string;
Если IsNotEmpty проваливается первым, асинхронный
constraint может вообще не вызываться из-за short-circuit поведения.
При диагностике важно учитывать, что class-validator не выводит внутренний поток выполнения. Поэтому полезно добавлять явные точки наблюдения:
async validate(value: string): Promise<boolean> {
console.log('start async check');
const result = await externalService.check(value);
console.log('end async check');
return result;
}
Если лог не появляется — проблема не в async-логике, а в том, что constraint не был зарегистрирован или не был вызван.
validateSync() вместо
validate()await при вызове validateasync: true в кастомных constraintsreturn в async-методахundefined вместо
boolean@ValidateNested() для вложенных DTO