Валидация в Joi строится вокруг возможности описывать зависимости между полями. Эти зависимости делятся на два основных класса: связи между полями одного уровня (siblings) и обращения к значениям вышестоящих объектов (ancestors). Именно они позволяют реализовывать межполевую логику: сравнение значений, условные ограничения и каскадную валидацию вложенных структур.
Siblings — это поля, находящиеся на одном уровне объекта. В Joi
доступ к ним реализуется через Joi.ref, позволяющий
ссылаться на другое свойство внутри того же объекта.
Joi.refconst schema = Joi.object({
password: Joi.string().min(8).required(),
confirmPassword: Joi.string().valid(Joi.ref('password')).required()
});
В этом примере confirmPassword сравнивается со значением
password. Оба поля являются siblings, так как принадлежат
одному объекту.
Ключевой момент: Joi.ref('password') интерпретируется
как путь внутри текущего объекта.
Частый сценарий — проверка диапазонов или логических ограничений между полями:
const schema = Joi.object({
startDate: Joi.date().required(),
endDate: Joi.date().min(Joi.ref('startDate')).required()
});
Здесь endDate зависит от startDate, и Joi
автоматически обеспечивает корректность порядка дат.
Метод when позволяет строить ветвления, основанные на
значениях других полей:
const schema = Joi.object({
role: Joi.string().valid('admin', 'user').required(),
accessLevel: Joi.when('role', {
is: 'admin',
then: Joi.number().min(10),
otherwise: Joi.number().min(1).max(5)
})
});
Поле accessLevel изменяет правила валидации в
зависимости от sibling role.
Joi.ref поддерживает дополнительные параметры, влияющие
на интерпретацию пути:
Joi.ref('password', { separator: '.' })
Это позволяет обращаться к вложенным структурам внутри siblings:
const schema = Joi.object({
user: Joi.object({
password: Joi.string().required()
}),
confirm: Joi.string().valid(Joi.ref('user.password'))
});
Ancestors — это родительские уровни вложенности. В сложных структурах данных поле может зависеть не только от siblings, но и от значений выше по дереву.
const schema = Joi.object({
account: Joi.object({
type: Joi.string().valid('premium', 'basic').required(),
limits: Joi.object({
requests: Joi.number().when(Joi.ref('/account/type'), {
is: 'premium',
then: Joi.number().max(10000),
otherwise: Joi.number().max(1000)
})
})
})
});
Здесь используется абсолютная ссылка /account/type,
которая выходит за пределы текущего объекта limits и
обращается к ancestor.
Joi поддерживает два подхода:
Начинается с / и игнорирует текущий уровень
вложенности:
Joi.ref('/user/settings/theme')
Интерпретируется относительно текущего объекта:
Joi.ref('settings.theme')
В сложных структурах важно управлять тем, на каком уровне выполняется разрешение ссылки.
Joi.ref('type', { ancestor: 1 })
Параметр ancestor указывает, насколько уровней вверх
подниматься при поиске значения:
0 — текущий объект1 — родитель2 — прародительКомбинирование обоих механизмов позволяет строить многоуровневую бизнес-логику.
const schema = Joi.object({
organization: Joi.object({
plan: Joi.string().valid('free', 'pro').required(),
user: Joi.object({
role: Joi.string().required(),
quota: Joi.number().when(Joi.ref('/organization/plan'), {
is: 'free',
then: Joi.number().max(100),
otherwise: Joi.number().max(10000)
})
})
})
});
Здесь:
plan и user находятся как siblings внутри
organizationquota зависит от ancestor
/organization/planСсылки в Joi чувствительны к наличию данных:
ref
становится undefinedvalid(Joi.ref(...)) может не пройти валидацию без
явного requiredСложные схемы часто строятся как цепочки зависимостей:
const schema = Joi.object({
a: Joi.number().required(),
b: Joi.number().min(Joi.ref('a')),
c: Joi.number().min(Joi.ref('b')),
d: Joi.number().min(Joi.ref('c'))
});
Здесь формируется каскад siblings-зависимостей, где каждое поле опирается на предыдущее.
Joi.ref('user.password') // ошибка при отсутствии структуры user
Joi.ref('type', { ancestor: 5 }) // выходит за пределы дерева
Это приводит к неожиданным результатам при глубокой вложенности.
В случаях, когда Joi.ref и when
недостаточны, применяется кастомная логика:
Joi.object({
a: Joi.number(),
b: Joi.number()
}).custom((value, helpers) => {
if (value.b < value.a) {
return helpers.error('any.invalid');
}
return value;
});
Этот подход полностью снимает ограничения на ancestors/siblings, но требует ручного управления логикой.