Обработка ошибок валидации

Библиотека формирует единый тип результата при нарушении правил валидации — массив объектов ValidationError. Именно этот формат становится основой всей дальнейшей обработки.

Каждый элемент содержит ключевые поля:

  • property — имя свойства, не прошедшего проверку
  • value — фактическое значение
  • constraints — набор сообщений об ошибках по конкретным декораторам
  • children — вложенные ошибки для объектов и массивов
  • target — исходный объект (часто исключается в production)

Типичная структура:

export interface ValidationError {
  target?: object;
  property: string;
  value?: any;
  constraints?: {
    [type: string]: string;
  };
  children?: ValidationError[];
}

Именно поле constraints становится точкой входа для формирования пользовательских сообщений.


Базовая обработка массива ValidationError

После вызова validate() или validateOrReject() результатом становится массив ошибок.

import { validate } from "class-validator";

const errors = await validate(userDto);

Пустой массив означает успешную проверку. Непустой — наличие нарушений.

Извлечение сообщений

Простейший вариант обработки — извлечение всех текстов:

function extractMessages(errors) {
  return errors.flatMap(error => {
    if (!error.constraints) return [];

    return Object.values(error.constraints);
  });
}

Результат:

[
  "email must be an email",
  "password must be longer than 8 characters"
]

Рекурсивная обработка вложенных ошибок

Вложенные структуры появляются при использовании:

  • объектов внутри DTO
  • массивов DTO
  • @ValidateNested()

Пример DTO:

class Address {
  @IsString()
  city: string;
}

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

Ошибки в address попадут в children.

Рекурсивный парсинг

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

  const traverse = (errs) => {
    for (const err of errs) {
      if (err.constraints) {
        result.push({
          property: err.property,
          messages: Object.values(err.constraints),
        });
      }

      if (err.children && err.children.length) {
        traverse(err.children);
      }
    }
  };

  traverse(errors);
  return result;
}

Формирование структурированных ответов API

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

Пример стандартизации:

function formatValidationErrors(errors) {
  const formatted = {};

  for (const error of errors) {
    if (error.constraints) {
      formatted[error.property] = Object.values(error.constraints);
    }

    if (error.children?.length) {
      formatted[error.property] = formatValidationErrors(error.children);
    }
  }

  return formatted;
}

Результат:

{
  "email": ["must be an email"],
  "address": {
    "city": ["should not be empty"]
  }
}

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

Метод validateOrReject выбрасывает исключение при наличии ошибок.

import { validateOrReject } from "class-validator";

await validateOrReject(dto);

При нарушении возвращается Promise.reject с массивом ошибок.

Перехват исключений

try {
  await validateOrReject(dto);
} catch (errors) {
  console.log(errors);
}

Это удобно при построении сервисного слоя, где требуется прерывание выполнения.


Обработка ошибок в middleware и контроллерах

Express-подход

app.post("/user", async (req, res) => {
  const dto = plainToInstance(UserDto, req.body);

  const errors = await validate(dto);

  if (errors.length > 0) {
    return res.status(400).json({
      errors: formatValidationErrors(errors),
    });
  }

  res.send("OK");
});

NestJS и ValidationPipe

В рамках NestJS обработка централизуется через ValidationPipe.

app.useGlobalPipes(new ValidationPipe());

Стандартное поведение

При ошибке выбрасывается BadRequestException, содержащая массив ValidationError.


Кастомизация формата ошибок через exceptionFactory

NestJS позволяет изменить структуру ответа:

new ValidationPipe({
  exceptionFactory: (errors) => {
    const formatted = errors.map(err => ({
      field: err.property,
      messages: err.constraints
        ? Object.values(err.constraints)
        : [],
    }));

    return new BadRequestException(formatted);
  },
});

Это полностью переопределяет стандартный формат обработки.


stopAtFirstError и оптимизация потока ошибок

Флаг:

new ValidationPipe({
  stopAtFirstError: true,
});

Изменяет поведение валидации:

  • проверка останавливается при первом нарушении
  • уменьшается количество вычислений
  • сокращается объём ошибок в ответе

Это влияет на структуру массива: вместо полного дерева формируется минимальный результат.


whitelist и forbidNonWhitelisted как часть обработки ошибок

Эти параметры влияют на генерацию ошибок на уровне полей DTO.

new ValidationPipe({
  whitelist: true,
  forbidNonWhitelisted: true,
});

Поведение:

  • whitelist — удаляет лишние поля
  • forbidNonWhitelisted — вызывает ошибку при наличии лишних полей

Ошибки такого типа приходят как стандартные ValidationError, но с системными сообщениями.


Контекст ошибок и пользовательские сообщения

Декораторы позволяют задавать кастомные сообщения:

@IsEmail({}, { message: "Некорректный email-формат" })
email: string;

В constraints попадёт именно заданный текст.


Сложные случаи: несколько constraints на одно поле

Одно поле может иметь несколько правил:

@IsString()
@Length(5, 20)
password: string;

Результат:

constraints: {
  isString: "password must be a string",
  length: "password must be longer than 5 characters"
}

При обработке важно учитывать множественность значений, а не только первое сообщение.


Агрегация ошибок по уровням

При больших DTO полезна группировка:

  • уровень 1: поле
  • уровень 2: правило
  • уровень 3: вложенные структуры

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

{
  user: {
    email: ["invalid email"],
    profile: {
      age: ["must be a number"]
    }
  }
}

Такой формат формируется через рекурсивный обход children.


Асинхронные валидаторы и ошибки

При использовании @Validate() с async логикой ошибки также возвращаются в стандартной форме:

@ValidatorConstraint({ async: true })
class UniqueEmailConstraint implements ValidatorConstraintInterface {
  async validate(email: string) {
    return false;
  }

  defaultMessage() {
    return "Email already exists";
  }
}

Ошибка попадёт в constraints как синхронная.


Потоки преобразования ошибок в доменную модель

В продакшн-системах часто вводится слой трансформации:

  • ValidationError[] → DTO ошибки API
  • DTO ошибки → лог-система
  • DTO ошибки → аналитика

Пример промежуточной модели:

type ApiValidationError = {
  field: string;
  messages: string[];
  code?: string;
};

Практика безопасной сериализации ошибок

Поле target обычно исключается, поскольку содержит исходный объект запроса.

Рекомендуемая фильтрация:

function sanitize(errors) {
  return errors.map(({ property, constraints }) => ({
    property,
    messages: constraints ? Object.values(constraints) : [],
  }));
}

Это предотвращает утечку внутреннего состояния объектов.


Особенности обработки при глубокой вложенности массивов

DTO с массивами:

class CreateOrder {
  @ValidateNested({ each: true })
  items: Item[];
}

Ошибки в этом случае содержат индексы:

items[0].price
items[1].name

При форматировании важно сохранять индексную структуру пути, иначе теряется контекст ошибки.