Библиотека class-validator возвращает ошибки в виде массива объектов
ValidationError, каждый из которых описывает одно
нарушенное правило валидации. Эти объекты имеют вложенную структуру и
требуют преобразования перед отправкой клиенту.
Основные поля ValidationError:
Пример результата валидации:
[
{
property: "email",
value: "not-an-email",
constraints: {
isEmail: "email must be an email"
}
}
]
Такой формат неудобен для фронтенда и требует нормализации.
Сырые ошибки class-validator обладают рядом особенностей, которые затрудняют их использование:
target,
value)В результате формируется задача приведения ошибок к стабильному контракту, например:
{
"status": "error",
"errors": [
{
"field": "email",
"messages": ["email must be an email"]
}
]
}
Первый шаг — извлечение сообщений из constraints.
function extractErrors(errors) {
return errors.map(err => {
const messages = err.constraints
? Object.values(err.constraints)
: [];
return {
field: err.property,
messages
};
});
}
Недостаток подхода — игнорирование вложенных объектов
(children).
При валидации сложных DTO ошибки могут быть вложенными:
class CreateUserDto {
@ValidateNested()
profile;
}
Структура ошибки:
{
property: "profile",
children: [
{
property: "firstName",
constraints: {
isNotEmpty: "firstName should not be empty"
}
}
]
}
Рекурсивная обработка:
function flattenErrors(errors, parentPath = "") {
let 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 && error.children.length > 0) {
result = result.concat(
flattenErrors(error.children, path)
);
}
}
return result;
}
Такой подход обеспечивает единый плоский список ошибок независимо от глубины структуры.
После нормализации ошибок формируется стабильный контракт ответа:
function buildErrorResponse(errors) {
return {
status: "error",
code: "VALIDATION_ERROR",
errors: flattenErrors(errors)
};
}
Пример результата:
{
"status": "error",
"code": "VALIDATION_ERROR",
"errors": [
{
"field": "email",
"messages": ["email must be an email"]
},
{
"field": "profile.firstName",
"messages": ["firstName should not be empty"]
}
]
}
class-validator позволяет задавать кастомные сообщения:
import { IsEmail } from "class-validator";
class UserDto {
@IsEmail({}, {
message: "Некорректный формат email"
})
email;
}
При этом в constraints попадёт именно пользовательское
сообщение, что упрощает дальнейшее форматирование.
Функция validate возвращает массив ошибок:
const errors = await validate(dto);
Функция validateOrReject выбрасывает исключение:
await validateOrReject(dto);
Для API-слоя чаще используется validate, так как
позволяет централизованно форматировать ответ.
Подход с единым обработчиком ошибок:
async function validateDto(dto) {
const errors = await validate(dto);
if (errors.length > 0) {
throw buildErrorResponse(errors);
}
}
Это позволяет стандартизировать все ответы без дублирования логики в контроллерах.
Типичный вариант ответа HTTP API:
res.status(400).json(
buildErrorResponse(errors)
);
Важно, чтобы структура ответа оставалась одинаковой во всех эндпоинтах.
Иногда требуется преобразование field в массив
путей:
function toArrayPath(field) {
return field.split(".");
}
Или более строгий формат:
{
field: ["profile", "firstName"]
}
Такой формат удобен для форм с вложенными структурами.
Некоторые поля не должны попадать в ответ:
targetvaluechildren (при плоском формате)Очистка объекта:
function sanitizeError(error) {
return {
property: error.property,
constraints: error.constraints
? Object.values(error.constraints)
: []
};
}
Вместо плоского списка возможна агрегация:
function groupErrors(errors) {
const result = {};
for (const err of flattenErrors(errors)) {
if (!result[err.field]) {
result[err.field] = [];
}
result[err.field].push(...err.messages);
}
return result;
}
Результат:
{
"email": ["email must be an email"],
"profile.firstName": ["should not be empty"]
}
При использовании интернационализации вместо строк могут использоваться ключи:
{
isEmail: "validation.email.invalid"
}
Далее на уровне клиента или сервера происходит маппинг:
function translateErrors(errors, t) {
return errors.map(err => ({
field: err.field,
messages: err.messages.map(t)
}));
}
Система валидации должна гарантировать:
errorsТиповой контракт:
type ErrorResponse = {
status: "error",
code: string,
errors: Array<{
field: string,
messages: string[]
}>
}
При работе с массивами ошибок property может содержать
индекс:
"items.0.name"
Это требует корректной обработки пути без потери структуры:
function normalizeArrayPaths(field) {
return field.replace(/\.(\d+)/g, "[$1]");
}
Результат:
items[0].name
При больших DTO рекурсивная обработка может быть затратной, поэтому применяются:
Оптимизированный шаблон:
function fastFlatten(errors, path = "", acc = []) {
for (const err of errors) {
const current = path ? `${path}.${err.property}` : err.property;
if (err.constraints) {
acc.push({
field: current,
messages: Object.values(err.constraints)
});
}
if (err.children?.length) {
fastFlatten(err.children, current, acc);
}
}
return acc;
}