Библиотека Joi предоставляет механизм расширения схем валидации через
создание пользовательских типов. Этот подход используется в случаях,
когда стандартных примитивов (string, number,
object, array и других) недостаточно для
описания предметной области. Пользовательские типы позволяют
инкапсулировать правила проверки, преобразования данных и обработку
ошибок внутри повторно используемых сущностей.
Механизм расширения реализуется через функцию
Joi.extend, которая принимает описание нового типа,
основанного на существующем или полностью кастомном поведении.
Создание нового типа начинается с вызова Joi.extend, где
описывается:
Пример минимальной структуры:
import Joi fr om 'joi';
const extendedJoi = Joi.extend({
type: 'positiveInt',
base: Joi.number(),
messages: {
'positiveInt.base': '"{{#label}}" должен быть положительным целым числом'
},
validate(value, helpers) {
if (!Number.isInteger(value) || value <= 0) {
return { value, errors: helpers.error('positiveInt.base') };
}
}
});
В этом примере создаётся новый тип positiveInt,
основанный на number, но с дополнительным ограничением:
значение должно быть положительным целым числом.
Поле base задаёт исходную схему, от которой наследуется
новый тип. Это позволяет повторно использовать встроенные механизмы Joi,
включая:
min, max,
required)Пример:
const JoiExtended = Joi.extend({
type: 'evenNumber',
base: Joi.number(),
validate(value, helpers) {
if (value % 2 !== 0) {
return { value, errors: helpers.error('evenNumber.base') };
}
},
messages: {
'evenNumber.base': 'Число должно быть чётным'
}
});
Использование base снижает объём ручной проверки и
позволяет интегрироваться с существующим API Joi.
Более гибкий способ создания типов — использование
rules. Каждый rule представляет отдельный метод, который
может быть вызван в цепочке.
Структура rule:
rules: {
ruleName: {
method(args) {
return this.$_addRule({ name: 'ruleName', args });
},
validate(value, helpers, args) {
return value;
}
}
}
const JoiExtended = Joi.extend({
type: 'sentence',
base: Joi.string(),
rules: {
maxWords: {
method(lim it) {
return this.$_addRule({ name: 'maxWords', args: { limit } });
},
args: [
{
name: 'limit',
assert: (value) => typeof value === 'number',
message: 'limit должен быть числом'
}
],
validate(value, helpers, args) {
const wordCount = value.trim().split(/\s+/).length;
if (wordCount > args.limit) {
return helpers.error('sentence.maxWords', { limit: args.limit });
}
return value;
}
}
},
messages: {
'sentence.maxWords': 'Количество слов не должно превышать {{#limit}}'
}
});
Использование:
const schema = JoiExtended.sentence().maxWords(5);
schema.validate('один два три четыре пять шесть');
Функция validate выполняется на уровне типа и
позволяет:
Сигнатура:
validate(value, helpers)
Пример использования для нормализации строки:
const JoiExtended = Joi.extend({
type: 'trimmedString',
base: Joi.string(),
validate(value, helpers) {
if (typeof value !== 'string') {
return { value, errors: helpers.error('string.base') };
}
return { value: value.trim() };
}
});
В данном случае значение модифицируется до передачи в дальнейшие проверки.
Пользовательские типы могут определять собственные сообщения через
поле messages.
Поддерживается интерполяция переменных:
{{#label}} — имя поля{{#value}} — текущее значениеhelpers.errorПример:
messages: {
'username.invalid': 'Имя пользователя "{{#value}}" недопустимо'
}
Передача параметров:
helpers.error('username.invalid', { value })
coerce позволяет преобразовывать входные данные до
основной валидации.
Пример: преобразование строки в число
const JoiExtended = Joi.extend({
type: 'numericString',
base: Joi.string(),
coerce(value, helpers) {
const parsed = Number(value);
if (!isNaN(parsed)) {
return { value: parsed };
}
return { value };
},
validate(value, helpers) {
if (typeof value !== 'number') {
return { value, errors: helpers.error('numericString.base') };
}
},
messages: {
'numericString.base': 'Значение должно быть числом'
}
});
Пользовательские типы могут комбинироваться с другими схемами Joi:
const JoiExtended = Joi.extend({
type: 'slug',
base: Joi.string(),
rules: {
format: {
validate(value, helpers) {
const isValid = /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value);
if (!isValid) {
return helpers.error('slug.format');
}
return value;
}
}
},
messages: {
'slug.format': 'Неверный формат slug'
}
});
const schema = JoiExtended.object({
url: JoiExtended.slug().format().required()
});
Несколько расширений могут объединяться через последовательные вызовы
extend:
const baseExtension = Joi.extend({
type: 'baseType',
base: Joi.any()
});
const finalExtension = baseExtension.extend({
type: 'customType',
base: baseExtension.baseType()
});
Такой подход позволяет строить многоуровневые системы валидации.
Пользовательские типы часто применяются для описания предметных областей:
Пример доменного типа:
const JoiExtended = Joi.extend({
type: 'orderStatus',
base: Joi.string().valid('new', 'processing', 'done', 'canceled'),
rules: {
notFinal: {
validate(value, helpers) {
if (value === 'done' || value === 'canceled') {
return helpers.error('orderStatus.final');
}
return value;
}
}
},
messages: {
'orderStatus.final': 'Изменение финального статуса запрещено'
}
});
Методы, добавленные через rules, сохраняют цепочный API
Joi:
const schema = JoiExtended.sentence()
.maxWords(10)
.required()
.messages({
'any.required': 'Поле обязательно'
});
Каждый вызов добавляет правило в очередь исполнения.
При валидации пользовательского типа порядок следующий:
coerce — преобразование данныхbase validation — базовая проверкаvalidate — кастомная логика типаrules — последовательные правилаrequired, min,
max, и др.)Понимание порядка важно при создании предсказуемых расширений.
Часто встречающиеся проблемы:
validate и
rulescoerceРациональная структура предполагает:
coerce — только преобразованиеvalidate — только базовые ограниченияrules — расширяемая логикаmessages — централизованные ошибкиПользовательские типы интегрируются в объекты без дополнительных настроек:
const schema = Joi.object({
username: JoiExtended.slug().required(),
score: JoiExtended.evenNumber().min(0)
});
Такой подход позволяет строить полностью типизированные доменные модели на основе расширений Joi.