При работе с объектными моделями данных в реальных приложениях редко встречаются плоские структуры. Чаще всего данные имеют иерархию: пользователь содержит профиль, профиль — адрес, заказ содержит список товаров и т.д. Простая валидация примитивных полей перестаёт быть достаточной, поскольку необходимо проверять корректность не только верхнего уровня объекта, но и всех вложенных сущностей.
В библиотеке class-validator для этих целей используется
декоратор @ValidateNested, который позволяет рекурсивно
применять правила валидации к вложенным объектам и массивам
объектов.
Без специальной настройки class-validator не выполняет
автоматическую проверку вложенных структур. Рассмотрим типичный
пример:
class Address {
street: string;
city: string;
zip: string;
}
class User {
name: string;
address: Address;
}
Если применить валидацию к User, то проверка затронет
только поле name. Поле address будет
проигнорировано, даже если внутри него находятся некорректные
значения.
Причина в том, что библиотека не создает экземпляры вложенных классов
автоматически и не знает, что поле address должно
валидироваться как отдельная сущность.
@ValidateNestedДекоратор @ValidateNested указывает библиотеке, что поле
содержит вложенный объект или массив объектов, которые также должны быть
валидированы.
import { ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';
class User {
@ValidateNested()
@Type(() => Address)
address: Address;
}
Ключевой момент заключается в сочетании двух инструментов:
@ValidateNested() — включает рекурсивную валидацию@Type(() => Class) из class-transformer
— обеспечивает корректное преобразование plain-объекта в экземпляр
классаБез @Type вложенный объект останется обычным
JavaScript-объектом, и валидаторы внутри Address не будут
применены.
import {
IsString,
Length,
ValidateNested
} from 'class-validator';
import { Type } from 'class-transformer';
class Address {
@IsString()
street: string;
@IsString()
city: string;
@Length(5, 10)
zip: string;
}
class User {
@IsString()
name: string;
@ValidateNested()
@Type(() => Address)
address: Address;
}
При такой конфигурации валидация затронет:
name в UserAddressclass-transformer в цепочке валидацииВалидация вложенных объектов в class-validator напрямую
зависит от преобразования данных.
Входные данные обычно приходят в виде plain object:
const payload = {
name: 'Alex',
address: {
street: 'Main',
city: 'NY',
zip: '12345'
}
};
После преобразования:
const user = plainToInstance(User, payload);
только после этого @ValidateNested способен корректно
пройти по дереву объектов.
Без этого шага address останется обычным объектом, и
декораторы внутри Address не будут активированы.
Частый сценарий — массив вложенных сущностей. Например, список адресов пользователя:
class User {
@ValidateNested({ each: true })
@Type(() => Address)
addresses: Address[];
}
each: trueОн определяет, что:
Addressclass Product {
@IsString()
title: string;
@IsNumber()
price: number;
}
class Order {
@ValidateNested({ each: true })
@Type(() => Product)
products: Product[];
}
Поведение:
products проходит собственную
валидациюСтруктуры могут иметь несколько уровней вложенности:
class Country {
@IsString()
name: string;
}
class Address {
@ValidateNested()
@Type(() => Country)
country: Country;
@IsString()
city: string;
}
class User {
@ValidateNested()
@Type(() => Address)
address: Address;
}
В этом случае валидация происходит рекурсивно:
User.addressAddress.countryКаждый уровень требует собственного @ValidateNested и
@Type.
@ValidateNested@TypeНаиболее частая ошибка:
class User {
@ValidateNested()
address: Address;
}
Результат:
address не преобразуется в класс@ValidateNestedclass User {
@Type(() => Address)
address: Address;
}
Результат:
Address не применяютсяeach@ValidateNested()
@Type(() => Address)
addresses: Address[];
Результат:
Ошибки формируются в виде дерева:
[
{
property: 'address',
children: [
{
property: 'zip',
constraints: {
length: 'zip must be longer than or equal to 5 characters'
}
}
]
}
]
Структура ошибок сохраняет путь до некорректного поля, что позволяет точно локализовать проблему в иерархии данных.
Если вложенный объект может быть опциональным, применяется комбинация декораторов:
import { IsOptional } from 'class-validator';
class User {
@IsOptional()
@ValidateNested()
@Type(() => Address)
address?: Address;
}
В этом случае:
address не вызывает ошибок@ValidateNested часто используется вместе с:
@IsArray() — для явного указания массива@IsOptional() — для необязательных полей@ArrayMinSize() / @ArrayMaxSize() — для
ограничения количества элементовПример:
class Order {
@IsArray()
@ArrayMinSize(1)
@ValidateNested({ each: true })
@Type(() => Product)
products: Product[];
}
@ValidateNested поддерживает рекурсивные модели,
например дерево категорий:
class Category {
@IsString()
name: string;
@ValidateNested({ each: true })
@Type(() => Category)
children: Category[];
}
Такая структура позволяет валидировать неограниченную глубину вложенности, при условии корректного формирования данных.
При большом количестве уровней вложенности и массивов объектов возрастает стоимость валидации:
В сложных структурах это становится заметным фактором, особенно при массовой обработке данных.
Если часть вложенных объектов валидна, а часть нет:
Это важно при обработке массивов, где требуется частичная обработка данных без полного отказа всей операции.
В архитектуре на основе DTO вложенная валидация становится стандартом:
class CreateUserDto {
@IsString()
name: string;
@ValidateNested()
@Type(() => CreateAddressDto)
address: CreateAddressDto;
}
Такой подход позволяет:
Использование @ValidateNested требует соблюдения
нескольких обязательных условий:
@Type{ each: true }Без соблюдения этих условий вложенная валидация либо не сработает, либо будет работать частично, что приведёт к некорректной проверке данных в сложных структурах.