Библиотека Joi предоставляет механизм расширения стандартной системы
валидации через пользовательские правила, позволяя внедрять сложную
бизнес-логику, которая не покрывается встроенными типами и методами.
Основной инструмент для этого — custom(), а также более
глубокий механизм расширений через Joi.extend().
Использование кастомной логики становится необходимым в ситуациях,
когда проверка данных зависит от внешних условий, комплексных вычислений
или нестандартных форматов. В отличие от декларативных правил
(min, max, email,
pattern), пользовательские валидаторы позволяют описывать
произвольное поведение валидации в виде функции.
Метод custom() применяется для добавления
пользовательской функции в цепочку валидации. Он принимает
функцию-валидатор, которая получает значение поля и набор
вспомогательных инструментов.
import Joi fr om 'joi';
const schema = Joi.object({
username: Joi.string().custom((value, helpers) => {
if (value.includes('admin')) {
return helpers.error('string.invalidUsername');
}
return value;
})
});
В этом примере проверяется наличие запрещённой подстроки. При
нарушении правила возвращается ошибка через
helpers.error().
Функция кастомной валидации получает второй аргумент — объект
helpers, предоставляющий доступ к вспомогательным
методам:
helpers.error(code, context) — генерация ошибкиhelpers.state — доступ к состоянию валидацииhelpers.prefs — пользовательские настройки схемыhelpers.original — исходное значение до
преобразованийJoi.string().custom((value, helpers) => {
const original = helpers.original;
if (original !== value.trim()) {
return helpers.error('string.whitespace');
}
return value;
});
Использование helpers.original позволяет сравнивать
исходные данные с преобразованными, что полезно при цепочках
trim(), lowercase() и других
модификаторах.
Кастомные валидаторы могут не только проверять, но и изменять данные. Возврат нового значения считается допустимым поведением.
Joi.number().custom((value, helpers) => {
return value * 2;
});
Здесь входное число трансформируется перед передачей дальше по схеме.
При необходимости можно сохранить исходную семантику и возвращать объект с метаданными:
Joi.string().custom((value, helpers) => {
return {
raw: value,
length: value.length
};
});
Кастомные валидаторы поддерживают асинхронную логику. Это важно при проверке данных через внешние сервисы или базы данных.
Joi.string().custom(async (value, helpers) => {
const exists = await checkUserInDatabase(value);
if (exists) {
return helpers.error('string.alreadyExists');
}
return value;
});
Асинхронный валидатор автоматически переводит схему в режим Promise.
const result = await schema.validateAsync(data);
Ошибки формируются через helpers.error(), где
указывается код и дополнительные параметры контекста.
Joi.number().custom((value, helpers) => {
if (value < 0) {
return helpers.error('number.negative', { lim it: 0 });
}
return value;
});
Код ошибки затем может быть обработан через кастомные сообщения:
const schema = Joi.number().messages({
'number.negative': 'Число не может быть меньше {#limit}'
});
Плейсхолдеры ({#limit}) берутся из контекста ошибки.
Кастомные функции выполняются в порядке цепочки правил. Это позволяет комбинировать стандартные и пользовательские проверки.
Joi.string()
.min(5)
.custom((value, helpers) => {
if (value === '12345') {
return helpers.error('string.weakPassword');
}
return value;
})
.max(20);
Сначала применяются встроенные правила, затем кастомная логика, после чего снова возможны встроенные проверки в зависимости от преобразований.
Более системный способ добавления кастомной логики — создание новых
типов через extend(). Этот подход используется для
повторного применения правил в разных схемах.
const customJoi = Joi.extend((joi) => ({
type: 'positiveString',
base: joi.string(),
validate(value, helpers) {
if (value.includes('-')) {
return { value, errors: helpers.error('string.noNegative') };
}
}
}));
Использование нового типа:
customJoi.object({
amount: customJoi.positiveString()
});
Расширения позволяют добавлять собственные методы в цепочку вызовов.
const extendedJoi = Joi.extend((joi) => ({
type: 'identifier',
base: joi.string(),
rules: {
alphanumericOnly: {
validate(value, helpers) {
if (!/^[a-z0-9]+$/i.test(value)) {
return helpers.error('string.alphanumeric');
}
return value;
}
}
}
}));
Использование:
extendedJoi.identifier().alphanumericOnly();
Кастомные валидаторы могут учитывать значения других полей через
state.ancestors или ref().
Joi.object({
password: Joi.string(),
confirm: Joi.string().custom((value, helpers) => {
const { password } = helpers.state.ancestors[0];
if (value !== password) {
return helpers.error('any.invalid');
}
return value;
})
});
Также используется Joi.ref() для декларативных
сравнений:
Joi.object({
password: Joi.string(),
confirm: Joi.string().valid(Joi.ref('password'))
});
helpers.state содержит информацию о текущем пути в
объекте, что важно при работе с вложенными структурами.
Joi.object({
user: Joi.object({
age: Joi.number().custom((value, helpers) => {
const path = helpers.state.path;
return value;
})
})
});
Это позволяет строить сложную диагностику ошибок и учитывать контекст вложенности.
Часто используемые сценарии:
Проверка форматов идентификаторов:
Joi.string().custom((value, helpers) => {
if (!value.startsWith('ID-')) {
return helpers.error('string.invalidFormat');
}
return value;
});
Проверка диапазонов с бизнес-логикой:
Joi.number().custom((value, helpers) => {
if (value % 10 !== 0) {
return helpers.error('number.notRounded');
}
return value;
});
Условная логика на основе внешних параметров:
const isProduction = true;
Joi.string().custom((value, helpers) => {
if (isProduction && value === 'debug') {
return helpers.error('string.forbidden');
}
return value;
});
Кастомные валидаторы в Joi формируют слой, позволяющий выходить за рамки декларативной схемы и внедрять произвольные правила, сохраняя при этом интеграцию с системой ошибок, цепочками преобразований и асинхронной моделью выполнения.