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

При работе с class-validator основная точка входа для пакетной проверки — функция validate, принимающая экземпляр класса и возвращающая массив ошибок ValidationError[]. При необходимости проверить сразу несколько объектов типовая ошибка заключается в попытке передать массив напрямую, что не поддерживается API библиотеки.

Корректная модель пакетной валидации строится через поэлементную обработку массива DTO:

import { validate } fr om "class-validator";

async function validateBatch(dtos) {
  const results = await Promise.all(
    dtos.map(dto => validate(dto))
  );

  return results;
}

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


Структура результата пакетной валидации

Возвращаемое значение после обработки массива выглядит как:

ValidationError[][]

Каждый вложенный массив соответствует конкретному объекту.

Пример структуры:

[
  [], // объект валиден
  [
    {
      property: "email",
      constraints: {
        isEmail: "email must be an email"
      }
    }
  ]
]

Такой формат требует дополнительной агрегации при формировании ответа API или логики бизнес-обработки.


Синхронная пакетная проверка через validateSync

При отсутствии асинхронных валидаторов возможен синхронный подход:

import { validateSync } fr om "class-validator";

function validateBatchSync(dtos) {
  return dtos.map(dto => validateSync(dto));
}

Особенности:

  • отсутствуют промисы
  • выполнение блокирует поток
  • не поддерживаются асинхронные ограничения (@ValidateIf с async логикой и т.п.)
  • подходит для небольших наборов данных

Использование validateOrReject для пакетной обработки

Метод validateOrReject выбрасывает исключение при наличии ошибок, что удобно для fail-fast логики:

import { validateOrReject } from "class-validator";

async function validateBatchOrReject(dtos) {
  await Promise.all(
    dtos.map(dto => validateOrReject(dto))
  );
}

Поведение:

  • при ошибке хотя бы одного объекта — промис отклоняется
  • остальные проверки могут не завершиться логически (в зависимости от планировщика событий)
  • требуется обработка через try/catch

Ограничение параллелизма при массовой валидации

При больших объёмах данных (тысячи объектов) использование Promise.all приводит к пиковым нагрузкам на CPU и память. Более устойчивый подход — контроль concurrency.

Пример реализации через ручной пул:

async function validateBatchWithLimit(dtos, lim it = 10) {
  const results = [];
  let index = 0;

  async function worker() {
    while (index < dtos.length) {
      const current = index++;
      const errors = await validate(dtos[current]);
      results[current] = errors;
    }
  }

  const workers = Array.from({ length: lim it }, worker);
  await Promise.all(workers);

  return results;
}

Такой подход:

  • стабилизирует нагрузку
  • предотвращает всплески памяти
  • улучшает предсказуемость времени выполнения

Валидация массивов внутри одного DTO

В class-validator поддерживается нативная валидация массивов через @ValidateNested({ each: true }).

Пример DTO:

import { Type } from "class-transformer";
import { ValidateNested, IsString } from "class-validator";

class ItemDto {
  @IsString()
  name;
}

class BatchDto {
  @ValidateNested({ each: true })
  @Type(() => ItemDto)
  items;
}

В этом случае валидация выполняется рекурсивно внутри одного вызова:

const errors = await validate(batchDto);

Особенности:

  • валидируется весь массив как единая структура
  • ошибки вложены в children
  • требуется class-transformer для корректной трансформации типов

Разбор структуры ValidationError в пакетном контексте

При пакетной обработке важно учитывать вложенность ошибок:

ValidationError {
  property: string;
  constraints?: Record<string, string>;
  children?: ValidationError[];
}

При работе с массивами DTO часто встречается структура:

  • верхний уровень — объект массива
  • второй уровень — поля DTO
  • третий уровень — вложенные DTO

Пример:

[
  {
    property: "items",
    children: [
      {
        property: "0",
        children: [
          {
            property: "name",
            constraints: {
              isString: "name must be a string"
            }
          }
        ]
      }
    ]
  }
]

Нормализация ошибок после пакетной проверки

Для API-слоёв требуется преобразование сложной структуры в плоский формат:

function flattenErrors(errors, parentPath = "") {
  const result = [];

  for (const error of errors) {
    const path = parentPath
      ? `${parentPath}.${error.property}`
      : error.property;

    if (error.constraints) {
      result.push({
        field: path,
        messages: Object.values(error.constraints)
      });
    }

    if (error.children?.length) {
      result.push(...flattenErrors(error.children, path));
    }
  }

  return result;
}

Применение:

const batch = await Promise.all(dtos.map(validate));
const flat = batch.map(flattenErrors);

Сопоставление результатов с исходными объектами

При пакетной обработке важно сохранять индексацию:

const indexed = dtos.map((dto, i) => ({
  index: i,
  dto,
  errors: batchResults[i]
}));

Это позволяет:

  • возвращать ошибки с привязкой к позиции в массиве
  • сохранять частичную валидность набора
  • реализовывать отчётность по batch-операциям

Частичная валидация и стратегия “мягкой ошибки”

В реальных системах часто требуется не прерывать обработку при ошибке одного элемента.

const results = await Promise.allSettled(
  dtos.map(dto => validate(dto))
);

Дальнейшая обработка:

const valid = [];
const invalid = [];

results.forEach((r, i) => {
  if (r.status === "fulfilled" && r.value.length === 0) {
    valid.push(dtos[i]);
  } else {
    invalid.push({
      dto: dtos[i],
      errors: r.status === "fulfilled" ? r.value : r.reason
    });
  }
});

Такой подход обеспечивает:

  • устойчивость batch-процессов
  • разделение успешных и ошибочных записей
  • возможность частичной записи в базу данных

Комбинирование с трансформацией данных

В большинстве архитектур пакетная валидация выполняется после трансформации:

import { plainToInstance } from "class-transformer";
import { validate } from "class-validator";

const instances = plainToInstance(DtoClass, rawArray);

const results = await Promise.all(
  instances.map(validate)
);

Критически важно:

  • трансформация должна соответствовать структуре DTO
  • несоответствие типов приводит к скрытым ошибкам валидации
  • вложенные массивы требуют @Type

Оптимизация больших наборов данных

При обработке больших batch-наборов применяются следующие техники:

  • разбиение на чанки фиксированного размера
  • ограничение параллелизма
  • предварительная фильтрация “пустых” объектов
  • кеширование повторяющихся структур DTO

Пример чанкинга:

function chunk(array, size) {
  const chunks = [];
  for (let i = 0; i < array.length; i += size) {
    chunks.push(array.slice(i, i + size));
  }
  return chunks;
}

async function validateInChunks(dtos) {
  const chunks = chunk(dtos, 50);
  const results = [];

  for (const group of chunks) {
    const res = await Promise.all(group.map(validate));
    results.push(...res);
  }

  return results;
}

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

При увеличении объёма данных проявляются характерные эффекты:

  • рост глубины дерева children
  • увеличение стоимости сериализации ошибок
  • дублирование одинаковых constraint-сообщений
  • необходимость агрегации по типу ошибки

Для оптимизации часто вводится постобработка:

function groupByConstraint(errors) {
  const map = new Map();

  for (const err of errors) {
    if (!err.constraints) continue;

    for (const msg of Object.values(err.constraints)) {
      map.set(msg, (map.get(msg) || 0) + 1);
    }
  }

  return map;
}

Архитектурная роль пакетной валидации

Пакетная проверка DTO в class-validator используется в нескольких слоях:

  • импорт больших массивов данных (CSV, API ingestion)
  • массовое создание сущностей
  • ETL-процессы
  • очереди сообщений и event-driven системы

Ключевая цель — разделение входного потока на валидные и невалидные элементы без деградации производительности и без блокировки всей операции при единичных ошибках.