Extend API в Joi представляет собой механизм расширения стандартного набора валидаторов и типов за счёт создания собственных правил, типов схем и поведения валидации. Этот инструмент используется тогда, когда встроенных возможностей библиотеки недостаточно для выражения предметной области или требуется переиспользуемая доменная логика валидации.
Расширение в Joi строится вокруг функции Joi.extend(),
которая принимает описание одного или нескольких пользовательских
расширений и возвращает новую инстанцию библиотеки с добавленными
возможностями. Важно понимать, что расширение не мутирует исходный
объект Joi, а создаёт его производную версию.
Базовая идея:
Joi.extendОбщая форма расширения выглядит следующим образом:
const ExtendedJoi = Joi.extend((joi) => ({
type: 'customType',
base: joi.string(),
messages: {
'customType.invalid': '{{#label}} имеет недопустимое значение'
},
rules: {
myRule: {
validate(value, helpers, args, options) {
return value;
}
}
}
}));
Ключевые элементы:
type — имя нового типа схемы;base — базовый тип Joi, от которого наследуется
поведение;messages — набор кастомных сообщений об ошибках;rules — расширяемые правила валидации;validate — функция, реализующая логику проверки.Создание нового типа позволяет описать семантически отдельную сущность, например email с дополнительными ограничениями, идентификатор формата или бизнес-объект.
Пример расширения для пользовательского типа «slug»:
const JoiSlug = Joi.extend((joi) => ({
type: 'slug',
base: joi.string(),
messages: {
'slug.invalid': '{{#label}} должен содержать только латиницу, цифры и дефисы'
},
validate(value, helpers) {
const isValid = /^[a-z0-9-]+$/.test(value);
if (!isValid) {
return { value, errors: helpers.error('slug.invalid') };
}
return { value };
}
}));
Использование:
const schema = JoiSlug.slug().required();
schema.validate('my-valid-slug'); // ok
schema.validate('invalid slug!'); // error
Rules позволяют добавлять цепочечные методы к существующему типу. Это ключевая возможность Extend API, обеспечивающая гибкость DSL-подобного синтаксиса.
Пример добавления правила минимальной длины без использования
встроенного min:
const Extended = Joi.extend((joi) => ({
type: 'string',
base: joi.string(),
rules: {
strictMin: {
method(length) {
return this.$_addRule({ name: 'strictMin', args: { length } });
},
args: [
{
name: 'length',
assert: (value) => typeof value === 'number',
message: 'length must be a number'
}
],
validate(value, helpers, args) {
if (value.length < args.length) {
return helpers.error('string.min', { limit: args.length });
}
return value;
}
}
}
}));
Использование:
const schema = Extended.string().strictMin(5);
schema.validate('abc'); // error
schema.validate('abcdef'); // ok
Функция validate внутри rules и type получает несколько
полезных параметров:
value — текущее значение;helpers — набор утилит для генерации ошибок и
преобразований;args — аргументы правила;state — состояние валидации (цепочка вызовов);options — глобальные опции валидации.Пример использования helpers:
validate(value, helpers) {
if (value < 0) {
return helpers.error('number.negative');
}
return value;
}
Сообщения задаются через messages, используя ключи,
связанные с правилами или типами:
messages: {
'string.strictMin': '{{#label}} должен быть не менее {{#limit}} символов'
}
Подстановки:
{{#label}} — имя поля;{{#value}} — текущее значение;{{#limit}} — пользовательский параметр.Extend API позволяет модифицировать поведение встроенных типов,
например string, number,
object.
Пример добавления правила к number:
const JoiExtended = Joi.extend((joi) => ({
type: 'number',
base: joi.number(),
rules: {
even: {
validate(value, helpers) {
if (value % 2 !== 0) {
return helpers.error('number.even');
}
return value;
}
}
},
messages: {
'number.even': '{{#label}} должно быть чётным числом'
}
}));
Пользовательские типы можно свободно использовать внутри
object() схем:
const schema = Joi.object({
id: JoiExtended.slug().required(),
age: JoiExtended.number().even()
});
Это делает расширения полностью интегрированными в систему Joi без дополнительных адаптеров.
Joi позволяет комбинировать несколько расширений последовательно:
const A = Joi.extend(extensionA);
const B = A.extend(extensionB);
Каждый новый слой сохраняет предыдущие расширения, формируя цепочку типов.
При сложной валидации полезен доступ к контексту:
state.path — путь к текущему полю;state.ancestors — родительские значения;state.reference — ссылки на другие части схемы.Пример проверки зависимостей:
validate(value, helpers, args, options) {
const parent = helpers.state.ancestors[0];
if (parent.role === 'admin' && value !== 'allowed') {
return helpers.error('any.invalid');
}
return value;
}
При использовании TypeScript расширение требует декларации новых типов:
declare module 'joi' {
interface StringSchema {
slug(): this;
}
}
Это обеспечивает корректную работу цепочек методов:
Joi.slug().required();
На практике Extend API используется для:
Типичные проблемы при использовании Extend API:
Extend API формирует уровень абстракции над базовыми примитивами Joi, позволяя переносить бизнес-логику в слой схем и создавать выразительные доменно-ориентированные валидаторы, интегрированные в стандартный механизм проверки данных.