Декораторы не работают

Основная причина, по которой декораторы в Class-validator «не работают», почти всегда связана не с самой библиотекой, а с тем, как JavaScript/TypeScript окружение обрабатывает декораторы на этапе компиляции и выполнения. Class-validator полностью опирается на механизм декораторов и метаданных типов, поэтому любое отклонение в конфигурации приводит к тому, что валидация становится «пустой» или вовсе не активируется.

TypeScript по умолчанию не включает поддержку декораторов в том виде, в котором их использует Class-validator. Если параметр experimentalDecorators выключен, декораторы просто игнорируются компилятором.

Ключевая настройка:

{
  "compilerOptions": {
    "experimentalDecorators": true
  }
}

При отсутствии этого флага код может выглядеть корректно, но фактически аннотации не будут применяться к классам и их свойствам. Это проявляется так:

  • @IsString() не валидирует строку
  • @IsNotEmpty() не срабатывает
  • объект проходит валидацию без ошибок

Визуально это воспринимается как «декораторы не работают», хотя они просто не существуют в рантайме.


Не включена генерация метаданных типов

Class-validator использует reflect-metadata для получения информации о типах свойств. Без этой информации многие декораторы не могут корректно интерпретировать данные.

Необходимая настройка:

{
  "compilerOptions": {
    "emitDecoratorMetadata": true
  }
}

Без неё происходит потеря типовой информации:

class User {
  @IsString()
  name: string;
}

В рантайме name становится просто undefined с точки зрения метаданных, и валидатор не может определить ожидаемый тип.


Не подключён reflect-metadata

Даже при правильном tsconfig.json библиотека reflect-metadata должна быть импортирована один раз до использования любых декораторов.

Типичная ошибка — забытый импорт:

import "reflect-metadata";

Этот импорт должен выполняться до объявления любых классов с декораторами. Особенно важно это в следующих средах:

  • Node.js приложения без фреймворков
  • тестовые среды (Jest, Vitest)
  • серверless-функции

Если импорт отсутствует, метаданные просто не регистрируются, и Class-validator получает «пустую» модель.


Неправильный порядок импортов

Проблема часто проявляется в проектах с большим количеством модулей. Если классы с декораторами импортируются до reflect-metadata, результат становится непредсказуемым.

Плохой сценарий:

import { User } from "./user";
import "reflect-metadata";

Правильный порядок:

import "reflect-metadata";
import { User } from "./user";

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


Использование сборщиков, удаляющих метаданные

Современные сборщики могут изменять поведение декораторов:

  • esbuild
  • SWC
  • Vite (в определённых конфигурациях)
  • Babel без корректных плагинов

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

  • удалены
  • не сгенерированы
  • переупакованы в несовместимом формате

Например, SWC требует явного включения поддержки legacy decorators:

{
  "jsc": {
    "transform": {
      "legacyDecorator": true,
      "decoratorMetadata": true
    }
  }
}

Если этого не сделать, Class-validator не сможет восстановить структуру классов.


Class-transformer не используется вместе с Class-validator

Во многих архитектурах Class-validator работает в связке с Class-transformer. Ошибка возникает, когда входные данные остаются обычными объектами без трансформации в классы.

Пример проблемного сценария:

const user = {
  name: "John"
};

validate(user);

В этом случае декораторы не применяются, потому что user — это не экземпляр класса.

Правильный подход:

import { plainToInstance } from "class-transformer";

const user = plainToInstance(User, {
  name: "John"
});

Без трансформации Class-validator не видит метаданных класса.


Валидация вызывается на обычном объекте

Даже если класс определён корректно, частая ошибка — передача plain object вместо instance.

class User {
  @IsString()
  name: string;
}

const dto = { name: 123 };

validate(dto); // декораторы не работают

Class-validator ожидает экземпляр:

const instance = Object.assign(new User(), dto);
validate(instance);

Tree-shaking и удаление классов

В production-сборках некоторые инструменты могут удалять «неиспользуемые» классы. Это особенно характерно для:

  • агрессивного tree-shaking
  • минификации
  • оптимизации серверных бандлов

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

Симптомы:

  • в dev работает
  • в production валидация «сломана»

Проблемы с ESM и CommonJS

Смешивание модулей может приводить к тому, что reflect-metadata подключается некорректно или поздно.

Типичная ситуация:

  • проект на ESM
  • библиотека ожидает CommonJS поведение
  • метаданные не привязываются к конструкторам

В таких случаях декораторы формально выполняются, но метаданные теряются.


Класс объявлен как функция или обёрнут неправильно

Некоторые паттерны нарушают работу декораторов:

const User = class {
  @IsString()
  name;
};

или фабричные обёртки:

function createUser() {
  class User {
    @IsString()
    name;
  }
  return User;
}

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


Конфликт версий TypeScript и Class-validator

Class-validator опирается на поведение декораторов, которое менялось между версиями TypeScript. Несовместимость приводит к ситуации, когда:

  • декоратор вызывается
  • но metadata key отличается
  • или Reflect API возвращает пустое значение

Особенно критично при переходе между:

  • TypeScript 4.x
  • TypeScript 5.x
  • legacy decorator mode vs stage 3 decorators

Отсутствие явного типа у свойства

Метаданные генерируются только при наличии явного типа. Следующий код может не работать корректно:

class User {
  @IsString()
  name;
}

Правильный вариант:

class User {
  @IsString()
  name: string;
}

Без типа string система не может вывести metadata design:type.


Асинхронная загрузка классов

Если классы загружаются динамически (lazy import), возможна ситуация, когда валидация выполняется до инициализации метаданных.

const module = await import("./user");
validate(new module.User());

Если reflect-metadata не был загружен заранее, результат будет непредсказуем.


Несовместимость с runtime без Reflect API

Некоторые окружения (старые браузеры, урезанные рантаймы) не поддерживают Reflect API. Class-validator полностью зависит от:

  • Reflect.defineMetadata
  • Reflect.getMetadata

При отсутствии этих функций декораторы формально выполняются, но не сохраняют данные.


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

Сбой декораторов в Class-validator почти всегда является следствием разрыва между тремя слоями:

  • TypeScript-компиляция (декораторы должны быть включены)
  • Runtime Reflect metadata (должен быть доступен и подключён)
  • Фактический экземпляр класса (а не plain object)

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