Преобразование вложенных структур

Вложенные структуры данных валидации в экосистеме декораторов требуют отдельного подхода, поскольку простые правила проверки примитивов не охватывают сценарии, где объект содержит другие объекты или массивы объектов.

Механизм валидации ориентирован на работу с классами и их экземплярами, а не с «сырыми» объектами JavaScript. Это создаёт ключевое условие: вложенные структуры должны быть преобразованы в экземпляры соответствующих классов до запуска валидации.

Основная проблема возникает при наличии следующей структуры:

class Address {
  street: string;
  city: string;
}

class User {
  name: string;
  address: Address;
}

Без дополнительной трансформации поле address остаётся обычным объектом, а не экземпляром Address, что делает невозможным применение вложенных правил.

Декоратор ValidateNested и его роль

Ключевой механизм обработки вложенных объектов — ValidateNested. Он активирует рекурсивную валидацию свойств, помеченных как сложные типы.

import { ValidateNested } from "class-validator";

class User {
  @ValidateNested()
  address: Address;
}

Сам по себе декоратор не выполняет преобразование типов. Он лишь инициирует рекурсивную проверку уже существующего экземпляра.

Необходимость преобразования типов

Для корректной работы вложенной валидации требуется преобразование plain-object → class instance. Это реализуется через class-transformer.

import { Type } from "class-transformer";

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

Декоратор Type определяет, в какой класс должен быть преобразован вложенный объект. Без него вложенные поля останутся объектами без прототипа класса, и правила валидации внутри Address не будут применены.

Вложенные массивы объектов

Отдельного внимания требуют коллекции, содержащие сложные структуры.

class User {
  @ValidateNested({ each: true })
  @Type(() => Address)
  addresses: Address[];
}

Флаг each: true указывает, что валидация должна применяться к каждому элементу массива. Без него массив рассматривается как единое значение, и вложенная логика игнорируется.

Комбинирование вложенных уровней

Сложные модели часто формируют многоуровневую структуру:

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

class Address {
  @ValidateNested()
  @Type(() => City)
  city: City;
}

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

В таких конструкциях валидация выполняется рекурсивно сверху вниз. Каждый уровень требует собственного @Type, иначе цепочка преобразования разрывается.

Особенности рекурсивной валидации

Рекурсивная модель валидации работает по принципу обхода графа объектов. При этом соблюдаются следующие особенности:

  • каждый уровень должен быть явно описан как класс;
  • преобразование типов обязательно на каждом уровне вложенности;
  • отсутствие @Type на любом уровне блокирует дальнейшую рекурсию;
  • @ValidateNested не инициирует преобразование, а только проверку.

Вложенные структуры с условной валидностью

В ряде случаев вложенные объекты могут быть необязательными:

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

Здесь порядок декораторов имеет значение. Сначала проверяется наличие значения, затем запускается вложенная валидация. При обратном порядке возможны ошибки при обработке undefined.

Обработка сложных массивов вложенных объектов

Массивы могут содержать объекты с собственной структурой:

class OrderItem {
  @IsString()
  productId: string;

  @IsNumber()
  quantity: number;
}

class Order {
  @ValidateNested({ each: true })
  @Type(() => OrderItem)
  items: OrderItem[];
}

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

Типичные ошибки при работе с вложенностью

На практике часто встречаются следующие проблемы:

  • отсутствие @Type, из-за чего вложенные классы не создаются;
  • пропуск each: true для массивов;
  • попытка валидировать plain-object без трансформации;
  • нарушение порядка декораторов, особенно при использовании @IsOptional;
  • смешивание интерфейсов TypeScript и классов, приводящее к потере метаданных.

Взаимодействие с преобразованием данных

Процесс подготовки данных к валидации обычно включает этап:

plain object → class-transformer → class instance → class-validator

На этом этапе критически важно, чтобы каждый вложенный объект получил корректный тип. Любое нарушение цепочки приводит к тому, что вложенные правила не активируются.

Глубокие структуры и производительность

При значительной глубине вложенности возникает дополнительная нагрузка на рекурсивную проверку. Каждый уровень увеличивает количество операций:

  • создание экземпляров классов;
  • применение декораторов;
  • рекурсивный обход дерева объектов.

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

Применение в сложных доменных моделях

В доменно-ориентированных структурах вложенность используется для описания агрегатов:

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

  @ValidateNested({ each: true })
  @Type(() => Address)
  addresses: Address[];
}

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

Контроль границ вложенной валидации

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

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

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

Поведение при частичной трансформации

Если часть вложенных объектов не преобразована, валидатор ведёт себя предсказуемо: проверка либо пропускается, либо выполняется некорректно. Наиболее критичным является сценарий, когда верхний уровень является экземпляром класса, а вложенные уровни остаются plain-object, что приводит к отсутствию рекурсивной проверки внутренних правил.