Отладка проблем валидации

Ошибки валидации в class-validator почти всегда связаны не с самой библиотекой, а с несоответствием между моделью данных и правилами декораторов. На практике наиболее частые источники проблем:

  • отсутствие преобразования входных данных в экземпляры классов
  • несоответствие типов (строка вместо числа, объект вместо примитива)
  • неверная настройка вложенной валидации
  • игнорирование асинхронных валидаторов
  • конфликт правил (whitelist, forbidNonWhitelisted)
  • неправильная обработка результатов validate()

Каждый из этих случаев проявляется одинаково: валидатор возвращает неожиданные ошибки или, наоборот, пропускает некорректные данные.


Проверка базового сценария валидации

Основной цикл работы библиотеки строится вокруг функции validate:

import { validate } from 'class-validator';
import { plainToInstance } from 'class-transformer';

class User {
  name;
}

const input = { name: 'Alex' };
const instance = plainToInstance(User, input);

validate(instance).then(errors => {
  console.log(errors);
});

Ключевая проблема возникает, когда пропускается этап преобразования через plainToInstance. В этом случае декораторы могут не срабатывать корректно, особенно при строгой типизации.


Проблемы с преобразованием типов

class-validator не выполняет автоматическое приведение типов. Значение "123" остаётся строкой, даже если ожидается число.

Типичный сбой:

import { IsNumber } from 'class-validator';

class Product {
  @IsNumber()
  price;
}

const input = { price: "100" };

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

Решение связано с использованием трансформации:

import { Type } from 'class-transformer';

class Product {
  @Type(() => Number)
  @IsNumber()
  price;
}

Анализ объекта ValidationError

Основной инструмент диагностики — структура ошибок:

[
  {
    property: 'price',
    constraints: {
      isNumber: 'price must be a number'
    },
    children: [],
    value: 'abc'
  }
]

Каждое поле содержит:

  • property — имя поля
  • constraints — набор нарушенных правил
  • value — фактическое значение
  • children — вложенные ошибки

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


Отладка вложенных объектов

При работе с вложенными структурами требуется явное указание @ValidateNested() и @Type().

import { ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';

class Address {
  street;
}

class User {
  @ValidateNested()
  @Type(() => Address)
  address;
}

Если @Type() отсутствует, вложенные валидаторы не активируются, и объект проходит проверку некорректно.


Поведение whitelist и forbidNonWhitelisted

Эти настройки часто создают иллюзию «сломавшейся» валидации.

import { validate } from 'class-validator';

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

Различие:

  • whitelist — удаляет лишние поля
  • forbidNonWhitelisted — выбрасывает ошибку при наличии лишних полей

Частая проблема возникает при неожиданных свойствах в payload. Ошибка при этом не связана с декораторами, а вызвана именно конфигурацией.


Асинхронные валидаторы и скрытые сбои

При использовании @Validate() с асинхронной логикой поведение становится зависимым от Promise.

import { ValidatorConstraint, ValidatorConstraintInterface } from 'class-validator';

@ValidatorConstraint({ async: true })
class IsUnique implements ValidatorConstraintInterface {
  async validate(value) {
    return await databaseCheck(value);
  }
}

Типичные ошибки:

  • отсутствие await при вызове validate
  • использование validateSync, который игнорирует async-валидаторы
  • непредсказуемый порядок выполнения проверок

Асинхронные правила всегда требуют использования validate, а не синхронной версии.


Различия между validate и validateSync

validate(instance);
validateSync(instance);

Сравнение:

  • validate — поддерживает async, возвращает Promise
  • validateSync — синхронный, игнорирует async-декораторы

Скрытая ошибка возникает, когда часть правил работает только в асинхронном режиме, но вызывается синхронный метод.


Отладка кастомных декораторов

Кастомные валидаторы часто содержат логику, которая трудно отслеживается в общей цепочке.

@ValidatorConstraint()
class IsEven {
  validate(value) {
    return value % 2 === 0;
  }
}

Для диагностики используется временное расширение:

  • логирование входного значения
  • проверка типа перед вычислением
  • изоляция валидатора вне общего класса

Основной источник ошибок — неожиданный тип входного значения.


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

По умолчанию многие декораторы игнорируют undefined, но не null.

@IsNotEmpty()
field;

Поведение:

  • undefined может пройти проверку
  • null почти всегда вызывает ошибку

Для строгого контроля используется комбинация:

@IsDefined()
@IsNotEmpty()
field;

Диагностика «тихих» ошибок

Ситуации, когда ошибок нет, но данные некорректны, возникают при:

  • отсутствии декораторов
  • отсутствии экземпляра класса (plain object)
  • пропущенной трансформации
  • использовании неверного уровня вложенности

Проверка выполняется через вывод итогового объекта после трансформации:

const instance = plainToInstance(User, input);
console.log(instance);

Если объект остаётся plain-структурой, валидация работает непредсказуемо.


Отладка через включение полного вывода ошибок

Расширенный анализ достигается через рекурсивный обход ValidationError:

function printErrors(errors) {
  errors.forEach(err => {
    console.log('Property:', err.property);
    console.log('Constraints:', err.constraints);

    if (err.children?.length) {
      printErrors(err.children);
    }
  });
}

Такой подход позволяет выявлять ошибки на любом уровне вложенности, включая скрытые поля.


Частые конфликты декораторов

Некоторые комбинации создают противоречивую логику:

  • @IsOptional() + @IsNotEmpty()
  • @Allow() + whitelist: true
  • @ValidateNested() без @Type()

Пример конфликтной ситуации:

@IsOptional()
@IsNotEmpty()
field;

Если поле отсутствует — пропускается. Если присутствует — не должно быть пустым. Такая комбинация часто приводит к ложным ожиданиям поведения.


Поведение при работе с массивами

Массивы требуют отдельной настройки:

import { IsArray, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';

class Item {
  name;
}

class Order {
  @IsArray()
  @ValidateNested({ each: true })
  @Type(() => Item)
  items;
}

Типичные ошибки:

  • отсутствие each: true
  • отсутствие @Type
  • передача не массива, а объекта

Контроль порядка выполнения валидаторов

class-validator не гарантирует строгий порядок выполнения правил. Это важно при:

  • зависимости одного правила от другого
  • кастомной логике с побочными эффектами

Поэтому валидаторы должны быть независимыми и не полагаться на последовательность.


Изоляция проблемного поля

Для поиска источника ошибки используется временное отключение правил:

  • комментирование декораторов
  • пошаговое добавление правил обратно
  • проверка одного поля в отдельном классе

Такой подход позволяет отделить проблемную логику от общей структуры модели.


Поведение при наследовании классов

При наследовании декораторы не всегда комбинируются интуитивно:

class Base {
  @IsString()
  id;
}

class User extends Base {
  @IsEmail()
  email;
}

Ошибки возникают при:

  • конфликте одноимённых полей
  • повторном объявлении свойств
  • отсутствии трансформации базового класса

Особое внимание требуется при глубокой иерархии моделей.


Диагностика через минимизацию модели

Эффективный способ выявления проблемы — временное упрощение класса до одного поля:

class DebugModel {
  @IsString()
  test;
}

Если валидация работает корректно, проблема локализуется в исходной структуре, а не в библиотеке.


Наблюдение за «поглощёнными» ошибками

Иногда ошибки не видны из-за обработки на уровне приложения:

  • фильтрация пустых массивов ошибок
  • перехват исключений
  • преобразование результата в другой формат

Проверка выполняется через прямой вывод результата validate без промежуточных слоёв.