Структура стандартного ответа class-validator опирается на массив объектов ValidationError, каждый из которых содержит информацию о полях, нарушивших правила валидации. При простом использовании библиотеки разработчик получает достаточно «сырые» данные, которые редко подходят для фронтенда или публичного API без дополнительной обработки.
Основная сложность заключается в том, что объект ошибки не является плоским. Он содержит вложенные структуры, где одно поле может порождать несколько ошибок, а каждая ошибка дополнительно содержит набор ограничений constraints, описывающих причины провала валидации.
Каждая ошибка валидации в class-validator обычно имеет следующую форму:
property — имя свойства объектаvalue — переданное значениеconstraints — объект с ключами правил и текстами
ошибокchildren — вложенные ошибки для объектов и
массивовtarget — исходный объект (в зависимости от
конфигурации)Главная особенность заключается в том, что children
делает структуру рекурсивной, а значит, прямое преобразование в плоский
JSON невозможно без обхода дерева.
Стандартный формат не подходит для большинства прикладных задач по следующим причинам:
Поэтому возникает необходимость создать собственный формат, который будет:
Первый шаг в создании собственного формата — обход дерева ошибок и извлечение значимых данных.
Типовая функция преобразования выглядит как рекурсивный обход:
function flattenErrors(errors) {
const result = [];
for (const error of errors) {
if (error.constraints) {
result.push({
field: error.property,
messages: Object.values(error.constraints),
});
}
if (error.children && error.children.length) {
result.push(...flattenErrors(error.children));
}
}
return result;
}
В этом подходе теряется структура вложенности, но формируется удобный список ошибок.
В реальных API часто требуется не просто список ошибок, а структурированный ответ:
Пример более развитого форматтера:
function formatValidationErrors(errors, parentPath = '') {
const formatted = [];
for (const error of errors) {
const path = parentPath
? `${parentPath}.${error.property}`
: error.property;
if (error.constraints) {
formatted.push({
field: path,
errors: Object.entries(error.constraints).map(
([rule, message]) => ({
rule,
message,
})
),
});
}
if (error.children && error.children.length) {
formatted.push(
...formatValidationErrors(error.children, path)
);
}
}
return formatted;
}
Такой формат сохраняет информацию о вложенности через
field, не усложняя структуру объекта.
Для интеграции в реальный проект обычно создаётся обёртка над результатом:
function createErrorResponse(errors) {
return {
success: false,
code: 'VALIDATION_ERROR',
timestamp: new Date().toISOString(),
errors: formatValidationErrors(errors),
};
}
Подобная структура позволяет стандартизировать все ответы API независимо от конкретного DTO.
В экосистеме NestJS наиболее распространённый способ кастомизации —
настройка ValidationPipe через exceptionFactory.
import { BadRequestException } from '@nestjs/common';
const validationPipe = new ValidationPipe({
exceptionFactory: (errors) => {
const formatted = formatValidationErrors(errors);
return new BadRequestException({
success: false,
code: 'VALIDATION_ERROR',
errors: formatted,
});
},
});
Такой подход полностью заменяет стандартный ответ фреймворка и позволяет централизованно управлять форматом.
При работе с вложенными DTO часто возникает необходимость сохранить иерархию. Для этого можно использовать древовидный формат:
function buildTree(errors) {
return errors.map(error => ({
field: error.property,
constraints: error.constraints
? Object.values(error.constraints)
: null,
children: error.children?.length
? buildTree(error.children)
: [],
}));
}
Такой формат полезен, когда фронтенд должен отображать ошибки в форме, соответствующей структуре данных.
class-validator может возвращать несколько ошибок на одно поле, поэтому важно нормализовать данные:
Пример агрегации:
function aggregateErrors(errors) {
const map = new Map();
for (const error of errors) {
const key = error.property;
if (!map.has(key)) {
map.set(key, []);
}
if (error.constraints) {
map.get(key).push(
...Object.values(error.constraints)
);
}
}
return Array.from(map.entries()).map(([field, messages]) => ({
field,
messages: [...new Set(messages)],
}));
}
При создании собственного формата часто возникает задача локализации. class-validator поддерживает пользовательские сообщения, которые можно переопределять на уровне декораторов.
Однако более гибкий подход заключается в постобработке:
const translations = {
isNotEmpty: 'Поле не должно быть пустым',
isEmail: 'Некорректный email',
};
function localizeErrors(errors) {
return errors.map(error => ({
field: error.property,
messages: Object.entries(error.constraints || {}).map(
([rule]) => translations[rule] || rule
),
}));
}
Такой слой позволяет отделить бизнес-логику от языковой логики.
В крупных приложениях форматирование ошибок выносят в отдельный сервис:
class ValidationFormatter {
static format(errors) {
return {
meta: {
count: errors.length,
},
errors: this.mapErrors(errors),
};
}
static mapErrors(errors, path = '') {
const result = [];
for (const error of errors) {
const currentPath = path
? `${path}.${error.property}`
: error.property;
if (error.constraints) {
result.push({
field: currentPath,
violations: Object.keys(error.constraints).map(
key => ({
type: key,
message: error.constraints[key],
})
),
});
}
if (error.children?.length) {
result.push(
...this.mapErrors(error.children, currentPath)
);
}
}
return result;
}
}
Такой класс позволяет централизовать правила преобразования и легко расширять их при росте системы.
При проектировании формата важно исключать внутренние поля:
target — объект запросаvalue — необработанное значениеОставляются только:
Это снижает связность API с внутренней реализацией class-validator.
На практике часто используется гибридный подход:
Пример универсального переключателя:
function format(errors, mode = 'flat') {
switch (mode) {
case 'tree':
return buildTree(errors);
case 'aggregate':
return aggregateErrors(errors);
default:
return formatValidationErrors(errors);
}
}
Такой подход позволяет адаптировать систему под разные клиенты без изменения основной логики валидации.
Создание пользовательского формата требует строгого контроля обратной совместимости. Любое изменение структуры может привести к поломке клиентских приложений. Поэтому формат обычно: