Работа с пользовательскими форматами вывода в Vest строится вокруг преобразования результата выполнения валидационных наборов (suites) в структуру, удобную для конкретного слоя приложения: UI-форм, API-ответов, логирования или интеграции с внешними системами.
Любой запуск vest.run() возвращает объект результата,
содержащий стандартные секции:
Каждый элемент ошибки обычно содержит:
field — имя поляmessage — текст ошибкиtest — идентификатор тестаgroup — группа (если использовалась группировка)Эта структура универсальна, но редко используется напрямую в UI без преобразования.
Стандартный формат результата не всегда подходит под конкретную задачу:
Поэтому результат Vest часто преобразуется в кастомный формат.
Наиболее распространённый формат — объект, где ключи являются именами полей:
function groupErrorsByField(result) {
return result.errors.reduce((acc, error) => {
if (!acc[error.field]) {
acc[error.field] = [];
}
acc[error.field].push(error.message);
return acc;
}, {});
}
Результат:
{
"email": ["Email обязателен", "Неверный формат email"],
"password": ["Слишком короткий пароль"]
}
Такой формат удобен для форм с отображением ошибок под каждым полем.
Иногда требуется полностью линейная структура:
function flattenErrors(result) {
return result.errors.map(error => ({
field: error.field,
message: error.message
}));
}
Используется в:
Часто требуется строгая контрактная структура:
function toApiResponse(result) {
return {
success: result.isValid,
errors: result.errors.map(e => ({
path: e.field,
detail: e.message
}))
};
}
Особенность такого формата — унификация ключей (path,
detail) независимо от внутренней структуры Vest.
Vest поддерживает валидацию вложенных структур, например:
user.emailuser.profile.nameaddresses[0].cityДля пользовательского форматирования важно учитывать пути.
function buildNestedErrors(errors) {
const result = {};
for (const error of errors) {
const path = error.field.split('.');
let current = result;
for (let i = 0; i < path.length; i++) {
const key = path[i];
if (i === path.length - 1) {
if (!current[key]) current[key] = [];
current[key].push(error.message);
} else {
if (!current[key]) current[key] = {};
current = current[key];
}
}
}
return result;
}
Результат:
{
"user": {
"email": ["Email обязателен"],
"profile": {
"name": ["Имя слишком короткое"]
}
}
}
Vest не ограничивает момент формирования сообщений только внутри тестов. Часто сообщения заменяются при выводе.
const messagesMap = {
REQUIRED: "Поле обязательно",
MIN_LENGTH: "Слишком короткое значение"
};
function localizeErrors(result) {
return result.errors.map(err => ({
field: err.field,
message: messagesMap[err.test] || err.message
}));
}
Это позволяет отделить бизнес-логику от слоя представления.
Для фронтенд-фреймворков часто используется структура:
function toFormState(result) {
return {
errors: groupByField(result.errors),
valid: result.isValid,
touched: {}
};
}
Дополнительно могут добавляться:
dirty состоянияwarningserrorCountИногда требуется показывать не все ошибки сразу.
function filterCriticalErrors(result) {
return result.errors.filter(e => e.severity === "critical");
}
function limitErrorsPerField(result, limit = 1) {
const grouped = groupErrorsByField(result);
for (const key in grouped) {
grouped[key] = grouped[key].slice(0, limit);
}
return grouped;
}
Для серверных систем часто важна трассировка:
function toLogFormat(result) {
return {
timestamp: Date.now(),
status: result.isValid ? "valid" : "invalid",
errors: result.errors.map(e => ({
field: e.field,
message: e.message,
test: e.test
}))
};
}
Такой формат удобен для:
В реальных приложениях часто используется комбинирование:
function formatResult(result) {
return {
api: toApiResponse(result),
form: groupErrorsByField(result),
log: toLogFormat(result)
};
}
Это позволяет одним запуском валидации обслуживать сразу несколько слоёв системы.
Перед преобразованием часто выполняется нормализация:
function normalizeErrors(errors) {
const seen = new Set();
return errors.filter(err => {
const key = `${err.field}:${err.message}`;
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}
На практике удобно строить слой форматирования как набор независимых функций:
const formatters = {
api: toApiResponse,
form: groupErrorsByField,
log: toLogFormat
};
function format(result, type) {
return formatters[type](result);
}
Такой подход позволяет:
Если в тестах добавляются дополнительные данные, они могут быть использованы при выводе:
test("email", "Email invalid", () => {
enforce(value).matches(/@/).message("invalid_email");
});
Позже:
function enrichErrors(result) {
return result.errors.map(e => ({
...e,
humanMessage: translate(e.message),
severity: getSeverity(e.test)
}));
}
При построении библиотек или API поверх Vest важно фиксировать контракт:
testsЭто позволяет менять внутреннюю реализацию suite без влияния на потребителей результата.