Валидация массивов

Работа с массивами в class-validator отличается от проверки скалярных значений тем, что требует одновременного контроля структуры контейнера и каждого его элемента. Библиотека предоставляет набор декораторов, которые позволяют описывать ограничения как на уровне массива, так и на уровне вложенных значений.

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


Проверка, что значение является массивом

Базовый декоратор для проверки типа — @IsArray().

import { IsArray } from 'class-validator';

export class CreateUserDto {
  @IsArray()
  roles: string[];
}

Декоратор проверяет только факт того, что значение является массивом. Он не накладывает ограничений на содержимое, длину или тип элементов.


Валидация элементов массива через each

Для применения правил к каждому элементу используется параметр each: true.

import { IsArray, IsString } from 'class-validator';

export class CreateUserDto {
  @IsArray()
  @IsString({ each: true })
  roles: string[];
}

В этом случае:

  • каждый элемент массива должен быть строкой
  • ошибка будет возвращена для конкретных элементов, нарушающих правило
  • сам массив остаётся валидируемым как единая сущность

Типичная ошибка — отсутствие each: true, что приводит к проверке всего массива как строки, объекта или другого типа.


Ограничение размера массива

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

  • @ArrayMinSize()
  • @ArrayMaxSize()
  • @ArrayNotEmpty()
import {
  IsArray,
  ArrayMinSize,
  ArrayMaxSize,
  ArrayNotEmpty,
  IsString,
} from 'class-validator';

export class CreateUserDto {
  @IsArray()
  @ArrayNotEmpty()
  @ArrayMinSize(2)
  @ArrayMaxSize(5)
  @IsString({ each: true })
  roles: string[];
}

Поведение:

  • ArrayNotEmpty() запрещает пустой массив
  • ArrayMinSize(n) задаёт минимальное количество элементов
  • ArrayMaxSize(n) ограничивает максимальное количество элементов

Эти декораторы работают независимо от проверки элементов массива.


Валидация массивов примитивов

Массивы строк, чисел и булевых значений — наиболее частый случай.

Массив строк

import { IsArray, IsString, Length } from 'class-validator';

export class CreatePostDto {
  @IsArray()
  @IsString({ each: true })
  @Length(3, 20, { each: true })
  tags: string[];
}

Каждый элемент массива проходит полный набор правил.


Массив чисел

import { IsArray, IsNumber, Min, Max } from 'class-validator';

export class CreateStatsDto {
  @IsArray()
  @IsNumber({}, { each: true })
  @Min(0, { each: true })
  @Max(100, { each: true })
  scores: number[];
}

Важно учитывать, что при работе с HTTP-запросами числа часто приходят как строки, поэтому может потребоваться преобразование через class-transformer.


Массивы объектов и вложенная валидация

Наиболее сложный и часто используемый сценарий — массив объектов.

Для корректной работы требуется комбинация:

  • @ValidateNested({ each: true })
  • @Type(() => Class) из class-transformer
import { IsArray, ValidateNested, IsString } from 'class-validator';
import { Type } from 'class-transformer';

class CommentDto {
  @IsString()
  text: string;
}

export class CreatePostDto {
  @IsArray()
  @ValidateNested({ each: true })
  @Type(() => CommentDto)
  comments: CommentDto[];
}

Механика работы:

  • @Type() преобразует plain object в экземпляр класса
  • @ValidateNested() запускает валидацию вложенных объектов
  • each: true обеспечивает обработку каждого элемента массива

Без @Type() вложенные объекты могут не валидироваться корректно, так как не будут преобразованы в экземпляры классов.


Вложенные массивы (многомерные структуры)

class-validator поддерживает работу с массивами массивов, но требует явного указания правил на каждом уровне.

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

export class MatrixDto {
  @IsArray()
  @IsArray({ each: true })
  @IsNumber({}, { each: true })
  matrix: number[][];
}

Более строгий вариант с контролем вложенности через DTO:

class RowDto {
  @IsNumber({}, { each: true })
  values: number[];
}

export class MatrixDto {
  @IsArray()
  @ValidateNested({ each: true })
  @Type(() => RowDto)
  matrix: RowDto[];
}

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


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

Когда стандартных декораторов недостаточно, применяются пользовательские валидаторы.

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

@ValidatorConstraint({ name: 'isUniqueArray', async: false })
export class IsUniqueArrayConstraint implements ValidatorConstraintInterface {
  validate(value: any[]) {
    return Array.isArray(value) && new Set(value).size === value.length;
  }

  defaultMessage(args: ValidationArguments) {
    return `${args.property} должен содержать уникальные значения`;
  }
}

export function IsUniqueArray() {
  return function (object: Object, propertyName: string) {
    registerDecorator({
      target: object.constructor,
      propertyName,
      validator: IsUniqueArrayConstraint,
    });
  };
}

Использование:

export class CreateDto {
  @IsArray()
  @IsUniqueArray()
  tags: string[];
}

Кастомные валидаторы особенно полезны для:

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

Частые ошибки при валидации массивов

Отсутствие each: true

@IsString()
roles: string[];

Ошибка: проверяется сам массив, а не его элементы.


Нет ValidateNested для объектов

@IsArray()
comments: CommentDto[];

Без @ValidateNested вложенные объекты не валидируются.


Отсутствие Type при преобразовании

@ValidateNested({ each: true })
comments: CommentDto[];

Без @Type(() => CommentDto) объекты остаются plain object и не проходят корректную проверку.


Несоответствие типов при JSON

HTTP-запросы часто передают массивы как строки:

{
  "scores": "1,2,3"
}

Без трансформации это не будет массивом, и @IsArray() вернёт ошибку.


Особенности поведения валидатора

  • порядок декораторов влияет на читаемость, но не на порядок выполнения проверок
  • each: true работает только с совместимыми валидаторами
  • вложенные ошибки формируются с указанием пути к элементу массива
  • глубокие структуры могут увеличивать стоимость валидации по времени

Практика комбинирования декораторов

Типичная конфигурация для массива DTO:

@IsArray()
@ArrayMinSize(1)
@ValidateNested({ each: true })
@Type(() => ItemDto)
items: ItemDto[];

Для примитивов:

@IsArray()
@IsString({ each: true })
@ArrayMaxSize(10)
tags: string[];

Для числовых ограничений:

@IsArray()
@IsNumber({}, { each: true })
@Min(0, { each: true })
values: number[];