Механизм сообщений в Joi строится на двух уровнях: базовые сообщения встроенных валидаторов и сообщения, определяемые в пользовательских расширениях. При создании кастомных типов именно второй уровень становится ключевым инструментом управления ошибками валидации.
Кастомный тип в Joi создаётся через Joi.extend. Внутри
расширения сообщения определяются на уровне типа и отдельных правил.
Базовая структура включает:
type — имя нового типаbase — базовый валидаторmessages — набор сообщений по умолчаниюrules — пользовательские правила валидацииconst Joi = require('joi');
const JoiPositiveInt = Joi.extend((joi) => ({
type: 'positiveInt',
base: joi.number(),
messages: {
'positiveInt.base': '{{#label}} должен быть положительным целым числом',
'positiveInt.max': '{{#label}} не может быть больше {{#limit}}',
},
rules: {
positive: {
validate(value, helpers) {
if (!Number.isInteger(value) || value <= 0) {
return helpers.error('positiveInt.base');
}
return value;
}
},
max: {
method(limit) {
return this.$_addRule({ name: 'max', args: { limit } });
},
args: [
{
name: 'limit',
assert: (v) => typeof v === 'number',
message: 'должен быть числом'
}
],
validate(value, helpers, args) {
if (value > args.limit) {
return helpers.error('positiveInt.max', { limit: args.limit });
}
return value;
}
}
}
}));
Joi использует интерполяцию через контекст ошибки. В сообщениях доступны специальные переменные:
{{#label}} — имя поля{{#value}} — текущее значение{{#limit}} — ограничение из аргументов правила{{#key}} — внутренний ключ ошибкиПример:
messages: {
'positiveInt.max': '{{#label}} = {{#value}} превышает допустимый предел {{#limit}}'
}
При срабатывании ошибки сообщение автоматически подставляет значения
из контекста, формируемого в helpers.error.
Внутри validate используется helpers.error,
который связывает правило с ключом сообщения:
validate(value, helpers) {
if (value < 0) {
return helpers.error('positiveInt.base');
}
return value;
}
Связь между строкой ошибки и сообщением устанавливается через ключ
positiveInt.base, определённый в messages.
Сообщения кастомного типа можно переопределять локально:
const schema = JoiPositiveInt.positive().max(10).messages({
'positiveInt.max': 'Слишком большое значение: максимум {{#limit}}'
});
Приоритет сообщений:
.messages() у схемыJoi.extend)Кастомный тип может содержать множество правил, каждое из которых имеет собственный набор сообщений:
messages: {
'positiveInt.base': 'Ошибка базового типа',
'positiveInt.positive': 'Значение должно быть > 0',
'positiveInt.max': 'Превышен максимум {{#limit}}',
'positiveInt.min': 'Меньше минимального значения {{#limit}}'
}
Каждое правило должно явно вызывать соответствующий ключ ошибки, иначе сообщение не будет найдено.
При вызове helpers.error можно передавать дополнительные
данные:
return helpers.error('positiveInt.max', {
limit: args.limit,
value
});
Эти параметры становятся доступны в шаблоне сообщения:
{{#limit}}{{#value}}Это позволяет создавать динамические сообщения без усложнения логики валидатора.
При расширении базового типа (например, joi.number()),
остаются доступны стандартные ключи сообщений:
number.basenumber.minnumber.maxИх можно переопределять внутри кастомного типа:
messages: {
'number.base': 'Ожидалось число, получено другое значение'
}
Таким образом кастомный тип может комбинировать собственные и встроенные сообщения.
Если helpers.error вызывает ключ, который не определён в
messages, результат будет зависеть от конфигурации Joi:
Это делает обязательным строгий контроль соответствия ключей и сообщений.
В сложных схемах удобно разделять сообщения по слоям:
messages: {
// базовый уровень
'positiveInt.base': '...',
// правила
'positiveInt.min': '...',
'positiveInt.max': '...',
// системные переопределения
'number.base': '...'
}
Такой подход упрощает поддержку кастомных типов при расширении библиотеки.
Joi поддерживает динамическую генерацию сообщений через функции:
messages: {
'positiveInt.max': (context) =>
`${context.label} превышает максимум ${context.limit}`
}
Функциональный формат полезен при сложной логике формирования текста, когда шаблонов недостаточно.
При комбинировании кастомных типов с .concat() или
.alter() сообщения могут конфликтовать. Приоритет
сохраняется по правилу:
Это важно учитывать при построении сложных схем расширений.
При разработке кастомных типов в Joi логически разделяются три слоя сообщений:
Такое разделение позволяет сохранять предсказуемость поведения ошибок даже при глубокой композиции типов и повторном использовании расширений.