Вложенные структуры данных валидации в экосистеме декораторов требуют отдельного подхода, поскольку простые правила проверки примитивов не охватывают сценарии, где объект содержит другие объекты или массивы объектов.
Механизм валидации ориентирован на работу с классами и их экземплярами, а не с «сырыми» объектами JavaScript. Это создаёт ключевое условие: вложенные структуры должны быть преобразованы в экземпляры соответствующих классов до запуска валидации.
Основная проблема возникает при наличии следующей структуры:
class Address {
street: string;
city: string;
}
class User {
name: string;
address: Address;
}
Без дополнительной трансформации поле address остаётся
обычным объектом, а не экземпляром Address, что делает
невозможным применение вложенных правил.
Ключевой механизм обработки вложенных объектов —
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 для массивов;@IsOptional;Процесс подготовки данных к валидации обычно включает этап:
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, что приводит к отсутствию рекурсивной проверки внутренних правил.