Валидационные ошибки в class-validator представляют собой иерархическую структуру объектов, которая требует преобразования перед отправкой в API-ответе. Прямое возвращение этих объектов клиенту приводит к избыточности данных, нестабильному формату и сложности интерпретации на фронтенде. Поэтому ключевая задача — привести ошибки к единому, предсказуемому и сериализуемому виду.
Базовая ошибка в class-validator представлена объектом
ValidationError, который содержит несколько важных
полей:
property — имя поля, где возникла ошибкаconstraints — набор правил, которые не прошли
проверкуchildren — вложенные ошибки для объектов и
массивовvalue — исходное значениеtarget — объект, который валидировалсяТипичная структура выглядит так:
{
property: "email",
constraints: {
isEmail: "email must be an email",
isNotEmpty: "email should not be empty"
},
children: [],
value: "not-an-email"
}
При сложных DTO с вложенными объектами появляется
children, формируя дерево ошибок, которое необходимо
рекурсивно обходить.
Возврат ValidationError[] напрямую приводит к ряду
проблем:
target,
value)API должен возвращать плоскую и стабильную структуру, независимую от реализации валидатора.
На практике чаще всего используется единый формат:
{
"statusCode": 400,
"message": "Validation failed",
"errors": [
{
"field": "email",
"messages": [
"email must be an email",
"email should not be empty"
]
}
]
}
Такой формат обеспечивает:
Основная задача — рекурсивно пройти по дереву
ValidationError и собрать ошибки.
function formatErrors(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 > 0) {
const childrenErrors = formatErrors(error.children).map(child => ({
field: `${error.property}.${child.field}`,
messages: child.messages
}));
result.push(...childrenErrors);
}
}
return result;
}
При работе с DTO вида:
class Address {
@IsNotEmpty()
city: string;
}
class User {
@ValidateNested()
address: Address;
}
ошибки будут вложенными:
address: {
children: [
{
property: "city",
constraints: {
isNotEmpty: "city should not be empty"
}
}
]
}
После форматирования:
{
"field": "address.city",
"messages": ["city should not be empty"]
}
Для сложных структур важно корректно формировать путь поля. Используются два подхода:
user.address.city
Преимущества:
user.addresses[0].city
Используется при валидации массивов:
if (Array.isArray(error.children)) {
// индекс может быть частью property или metadata
}
class-validator не всегда явно передаёт индекс элемента массива, поэтому часто требуется дополнительная логика.
Пример структуры:
users: [
{
email: {
constraints: {
isEmail: "invalid email"
}
}
}
]
Форматирование:
{
"field": "users[0].email",
"messages": ["invalid email"]
}
Для этого обычно требуется обогащение DTO контекстом индекса на уровне бизнес-логики или кастомных валидаторов.
В API-формате обычно исключаются поля:
targetvaluechildren (после обработки)constraints (после преобразования)Пример очистки:
function sanitizeError(error) {
return {
property: error.property,
constraints: error.constraints
? Object.values(error.constraints)
: undefined
};
}
Альтернативный формат API — группировка сообщений:
{
"email": [
"must be an email",
"should not be empty"
],
"password": [
"too short"
]
}
Реализация:
function groupErrors(errors) {
const result = {};
for (const error of errors) {
const messages = error.constraints
? Object.values(error.constraints)
: [];
if (!result[error.property]) {
result[error.property] = [];
}
result[error.property].push(...messages);
if (error.children?.length) {
const childErrors = groupErrors(error.children);
for (const [key, value] of Object.entries(childErrors)) {
const fullKey = `${error.property}.${key}`;
if (!result[fullKey]) {
result[fullKey] = [];
}
result[fullKey].push(...value);
}
}
}
return result;
}
class-validator поддерживает кастомные сообщения, что позволяет внедрять i18n-слой.
Пример:
@IsEmail({}, {
message: "validation.email.invalid"
})
email: string;
Далее на уровне API можно производить трансляцию:
function localizeMessages(messages, t) {
return messages.map(msg => t(msg));
}
где t — функция перевода.
{
"field": "email",
"messages": ["email must be an email"],
"value": "test",
"constraints": {
"isEmail": "email must be an email"
}
}
{
"field": "email",
"messages": ["Invalid email"]
}
Разделение позволяет сохранить диагностическую информацию без утечки внутренних деталей в продакшене.
В типичном middleware или pipe:
import { validate } from "class-validator";
async function validateDto(dto) {
const errors = await validate(dto);
if (errors.length > 0) {
throw {
statusCode: 400,
errors: formatErrors(errors)
};
}
}
В реальных приложениях это часто интегрируется в глобальный обработчик исключений.
Для масштабируемых API вводится единый контракт:
statusCodeerrorCodemessageerrorsПример:
{
"statusCode": 400,
"errorCode": "VALIDATION_ERROR",
"message": "Request validation failed",
"errors": [
{
"field": "email",
"messages": ["Invalid email"]
}
]
}
Такой подход обеспечивает независимость от конкретной библиотеки валидации и упрощает миграции.
Некоторые системы используют режим stopAtFirstError,
однако class-validator по умолчанию собирает все ошибки. Это влияет на
форматирование:
Формирование корректного API-формата ошибок на основе class-validator требует строгой нормализации дерева ValidationError, рекурсивной обработки вложенных структур, удаления служебных полей и приведения результата к стабильному контракту, пригодному для фронтенда и внешних интеграций.