Валидация данных в схемах Joi строится на различии между
обязательными и необязательными полями. Классическое использование
.required() фиксирует жёсткое требование наличия значения,
тогда как .optional() допускает отсутствие поля без ошибок.
Однако в реальных сценариях структура данных редко остаётся статичной:
обязательность часто зависит от других значений внутри объекта.
Условная обязательность означает, что наличие или отсутствие поля определяется контекстом — значениями других полей, внешними параметрами или логическими связками внутри схемы. В Joi для этого используется набор механизмов, позволяющих описывать динамические правила в декларативной форме.
Ключевая особенность подхода заключается в том, что схема остаётся описательной: условия не вычисляются вручную, а задаются как часть декларации.
Основной инструмент условной логики в Joi — метод
.when(). Он позволяет менять поведение поля в зависимости
от значения другого поля.
Базовый синтаксис:
Joi.object({
type: Joi.string().valid('email', 'phone'),
email: Joi.string().email().when('type', {
is: 'email',
then: Joi.required(),
otherwise: Joi.optional()
}),
phone: Joi.string().when('type', {
is: 'phone',
then: Joi.required(),
otherwise: Joi.optional()
})
});
В этом примере обязательность email и phone
зависит от значения поля type. Логика становится
декларативной: правило описывает не процесс проверки, а условия его
применения.
when поддерживает сложные конструкции через массив
условий:
Joi.object({
role: Joi.string(),
accessLevel: Joi.number(),
secretKey: Joi.string().when('role', {
is: 'admin',
then: Joi.when('accessLevel', {
is: Joi.number().min(5),
then: Joi.required(),
otherwise: Joi.optional()
}),
otherwise: Joi.forbidden()
})
});
Здесь обязательность зависит сразу от двух факторов: роли и уровня доступа.
В Joi возможно ссылаться на значения других полей через
Joi.ref. Это особенно важно при сравнении значений или
вычислении условий на основе динамических данных.
Joi.object({
password: Joi.string().required(),
confirmPassword: Joi.string().valid(Joi.ref('password')).required()
});
Хотя этот пример не изменяет обязательность напрямую,
ref часто используется внутри when:
Joi.object({
minAge: Joi.number(),
maxAge: Joi.number().when('minAge', {
is: Joi.number().greater(18),
then: Joi.required(),
otherwise: Joi.optional()
})
});
Значение minAge влияет на обязательность
maxAge, создавая зависимость между полями.
Для более сложных сценариев применяется
Joi.alternatives(), позволяющий выбирать одну из нескольких
схем в зависимости от условий.
Joi.object({
contact: Joi.alternatives().conditional('preferred', {
is: 'email',
then: Joi.string().email().required(),
otherwise: Joi.string().pattern(/^\+?[0-9]+$/).required()
}),
preferred: Joi.string().valid('email', 'phone').required()
});
Альтернативы полезны, когда структура поля полностью меняется в зависимости от условий, а не только его обязательность.
Помимо when, Joi предоставляет набор логических
операторов, описывающих зависимости между полями.
Обязательное наличие хотя бы одного поля:
Joi.object({
email: Joi.string().email(),
phone: Joi.string()
}).or('email', 'phone');
Все указанные поля должны присутствовать одновременно:
Joi.object({
street: Joi.string(),
city: Joi.string(),
zip: Joi.string()
}).and('street', 'city', 'zip');
Только одно из полей должно быть указано:
Joi.object({
passport: Joi.string(),
driverLicense: Joi.string()
}).xor('passport', 'driverLicense');
Если одно поле присутствует, другое становится обязательным:
Joi.object({
cardNumber: Joi.string(),
cvv: Joi.string()
}).with('cardNumber', 'cvv');
Поля взаимоисключают друг друга:
Joi.object({
internalId: Joi.string(),
publicId: Joi.string()
}).without('internalId', 'publicId');
Эти конструкции формируют логическую систему ограничений, дополняя условную обязательность более строгими связями.
Joi позволяет учитывать внешний контекст при валидации через передачу
context в метод .validate().
const schema = Joi.object({
price: Joi.number().when(Joi.ref('$currency'), {
is: 'USD',
then: Joi.number().min(1),
otherwise: Joi.number().min(0)
})
});
schema.validate(
{ price: 10 },
{ context: { currency: 'USD' } }
);
Контекстные переменные ($currency) дают возможность
отделять данные объекта от внешних условий, сохраняя чистоту схемы.
В реальных приложениях условная обязательность часто строится из
нескольких уровней логики. Joi позволяет комбинировать
when, ref, логические операторы и
альтернативы.
Joi.object({
userType: Joi.string().valid('guest', 'registered', 'admin'),
email: Joi.string().email().when('userType', {
switch: [
{
is: 'guest',
then: Joi.required()
},
{
is: 'registered',
then: Joi.required()
}
],
otherwise: Joi.forbidden()
}),
adminCode: Joi.string().when('userType', {
is: 'admin',
then: Joi.when('email', {
is: Joi.exist(),
then: Joi.required(),
otherwise: Joi.forbidden()
})
})
});
Использование switch позволяет описывать множественные
ветки логики без вложенных конструкций.
Joi.object({
entityType: Joi.string().valid('person', 'company'),
firstName: Joi.string().when('entityType', {
is: 'person',
then: Joi.required(),
otherwise: Joi.forbidden()
}),
companyName: Joi.string().when('entityType', {
is: 'company',
then: Joi.required(),
otherwise: Joi.forbidden()
})
});
Joi.object({
hasDiscount: Joi.boolean(),
discountValue: Joi.number().when('hasDiscount', {
is: true,
then: Joi.required().min(1),
otherwise: Joi.optional()
})
});
Joi.object({
startDate: Joi.date(),
endDate: Joi.date().when('startDate', {
is: Joi.exist(),
then: Joi.required().greater(Joi.ref('startDate')),
otherwise: Joi.optional()
})
});
При усложнении бизнес-логики допустимо вложение when на
нескольких уровнях, где каждое последующее правило уточняет
предыдущее.
Joi.object({
subscription: Joi.string().valid('basic', 'pro'),
paymentMethod: Joi.string().when('subscription', {
is: 'pro',
then: Joi.when('billingCycle', {
is: 'monthly',
then: Joi.required(),
otherwise: Joi.optional()
})
}),
billingCycle: Joi.string().valid('monthly', 'yearly')
});
Такая структура позволяет формировать зависимости, близкие к бизнес-правилам предметной области.
Условная обязательность в Joi опирается на несколько устойчивых подходов:
whenref для динамических
сравненийor, and,
xoralternativescontextКаждый из этих механизмов работает в рамках единой декларативной модели, где логика описывается как структура ограничений, а не как последовательность инструкций.