При валидации сложных объектов часто требуется учитывать не только тип и формат отдельного поля, но и взаимосвязи между несколькими свойствами. Библиотека Joi предоставляет мощный набор механизмов для описания подобных зависимостей.
Зависимости позволяют:
Основные механизмы взаимосвязей работают на уровне
Joi.object().
Метод with() требует наличие одного поля вместе с
другим.
object.with(source, required)
source — поле-инициатор;required — обязательное поле.const Joi = require('joi');
const schema = Joi.object({
login: Joi.string(),
password: Joi.string()
}).with('login', 'password');
Если присутствует login, обязательно должен существовать
password.
Корректные данные:
{
login: 'admin',
password: '123456'
}
{}
Ошибка:
{
login: 'admin'
}
const schema = Joi.object({
username: Joi.string(),
password: Joi.string(),
email: Joi.string()
}).with('username', ['password', 'email']);
Теперь наличие username требует сразу два поля.
Метод without() запрещает наличие одного поля вместе с
другим.
const schema = Joi.object({
token: Joi.string(),
password: Joi.string()
}).without('token', 'password');
Если существует token, поле password
запрещено.
Допустимо:
{
token: 'abc123'
}
{
password: '123456'
}
Ошибка:
{
token: 'abc123',
password: '123456'
}
Метод and() требует совместного существования группы
полей.
const schema = Joi.object({
startDate: Joi.date(),
endDate: Joi.date()
}).and('startDate', 'endDate');
startDate, нужен
endDate;endDate, нужен startDate.Ошибка:
{
startDate: '2025-01-01'
}
Метод or() требует наличие хотя бы одного поля из
списка.
const schema = Joi.object({
email: Joi.string().email(),
phone: Joi.string()
}).or('email', 'phone');
{
email: 'user@mail.com'
}
{
phone: '+77001234567'
}
{
email: 'user@mail.com',
phone: '+77001234567'
}
{}
Метод xor() требует наличие только одного поля из
группы.
const schema = Joi.object({
email: Joi.string(),
phone: Joi.string()
}).xor('email', 'phone');
{
email: 'user@mail.com'
}
или
{
phone: '+77001234567'
}
Оба поля одновременно:
{
email: 'user@mail.com',
phone: '+77001234567'
}
Отсутствие обоих полей:
{}
Метод oxor() похож на xor(), но поля
становятся необязательными.
const schema = Joi.object({
email: Joi.string(),
phone: Joi.string()
}).oxor('email', 'phone');
{}
{
email: 'user@mail.com'
}
{
email: 'user@mail.com',
phone: '+77001234567'
}
Метод nand() запрещает совместное наличие полей.
const schema = Joi.object({
isAdmin: Joi.boolean(),
guest: Joi.boolean()
}).nand('isAdmin', 'guest');
{
isAdmin: true,
guest: true
}
Метод when() — основной инструмент для создания
динамических схем.
Joi.when(reference, options)
Параметры:
reference — поле для проверки;options — условия.const schema = Joi.object({
role: Joi.string(),
permissions: Joi.when('role', {
is: 'admin',
then: Joi.required(),
otherwise: Joi.forbidden()
})
});
Если:
{
role: 'admin'
}
то возникнет ошибка — отсутствует permissions.
Для обычного пользователя:
{
role: 'user'
}
поле permissions запрещено.
when() способен менять не только обязательность, но и
всю схему.
const schema = Joi.object({
type: Joi.string(),
value: Joi.when('type', {
is: 'number',
then: Joi.number(),
otherwise: Joi.string()
})
});
{
type: 'number',
value: 100
}
{
type: 'text',
value: 'hello'
}
Для сложной логики применяется switch.
const schema = Joi.object({
role: Joi.string(),
accessLevel: Joi.when('role', {
switch: [
{
is: 'admin',
then: Joi.number().min(10)
},
{
is: 'moderator',
then: Joi.number().min(5)
}
],
otherwise: Joi.number().min(1)
})
});
when() может анализировать объект целиком.
const schema = Joi.object({
country: Joi.string(),
city: Joi.string(),
zip: Joi.string()
}).when(
Joi.object({
country: Joi.valid('USA')
}).unknown(),
{
then: Joi.object({
zip: Joi.required()
})
}
);
Если страна — USA, индекс становится обязательным.
Joi.ref() позволяет ссылаться на значение другого
поля.
const schema = Joi.object({
password: Joi.string().required(),
confirmPassword: Joi.string()
.valid(Joi.ref('password'))
.required()
});
{
password: '123456',
confirmPassword: 'qwerty'
}
const schema = Joi.object({
min: Joi.number(),
max: Joi.number().greater(Joi.ref('min'))
});
max должен быть больше min.
const schema = Joi.object({
startDate: Joi.date(),
endDate: Joi.date().greater(Joi.ref('startDate'))
});
Вложенные объекты поддерживают относительные ссылки.
const schema = Joi.object({
credentials: Joi.object({
password: Joi.string(),
confirmPassword: Joi.string()
.valid(Joi.ref('password'))
})
});
Для обращения к корню схемы используется /.
const schema = Joi.object({
role: Joi.string(),
profile: Joi.object({
access: Joi.string().when(Joi.ref('/role'), {
is: 'admin',
then: Joi.required()
})
})
});
const schema = Joi.object({
tags: Joi.array().items(
Joi.object({
type: Joi.string(),
value: Joi.when('type', {
is: 'system',
then: Joi.string().required(),
otherwise: Joi.number().required()
})
})
)
});
const schema = Joi.object({
paymentType: Joi.string().required()
}).when(
Joi.object({
paymentType: Joi.valid('card')
}).unknown(),
{
then: Joi.object({
cardNumber: Joi.string().required(),
cvv: Joi.string().required()
}),
otherwise: Joi.object({
cashbox: Joi.string().required()
})
}
);
Иногда зависимости удобнее описывать альтернативными структурами.
const schema = Joi.alternatives().try(
Joi.object({
type: Joi.valid('email'),
email: Joi.string().email().required()
}),
Joi.object({
type: Joi.valid('phone'),
phone: Joi.string().required()
})
);
При невозможности выразить правило стандартными средствами
используется custom().
const schema = Joi.object({
start: Joi.number(),
end: Joi.number()
}).custom((value, helpers) => {
if (value.end <= value.start) {
return helpers.error('any.invalid');
}
return value;
});
Joi поддерживает передачу внешнего контекста.
const schema = Joi.object({
role: Joi.string()
}).when('$isAdmin', {
is: true,
then: Joi.object({
role: Joi.valid('admin')
})
});
Вызов:
schema.validate(data, {
context: {
isAdmin: true
}
});
Механизмы можно объединять.
const schema = Joi.object({
username: Joi.string(),
password: Joi.string(),
token: Joi.string(),
confirmPassword: Joi.string()
.valid(Joi.ref('password'))
}).with('username', 'password')
.without('token', 'password');
username требует password;token запрещает password;confirmPassword должен совпадать с
password.const schema = Joi.object({
password: Joi.string(),
confirmPassword: Joi.string()
.valid(Joi.ref('password'))
.messages({
'any.only': 'Пароли не совпадают'
})
});
const schema = Joi.object({
email: Joi.string(),
phone: Joi.string()
}).xor('email', 'phone')
.messages({
'object.xor': 'Укажите либо email, либо phone'
});
const schema = Joi.object({
login: Joi.string(),
password: Joi.string(),
token: Joi.string()
}).xor('password', 'token')
.with('login', 'password');
const schema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().min(8).required(),
confirmPassword: Joi.string()
.valid(Joi.ref('password'))
.required(),
age: Joi.number(),
parentConsent: Joi.boolean()
}).when(
Joi.object({
age: Joi.number().less(18)
}).unknown(),
{
then: Joi.object({
parentConsent: Joi.valid(true).required()
})
}
);
const schema = Joi.object({
from: Joi.date(),
to: Joi.date(),
limit: Joi.number(),
offset: Joi.number()
})
.and('fr om', 'to')
.with('offset', 'lim it');
Ошибка:
Joi.ref('user.password')
при отсутствии вложенного объекта.
.with('a', 'b')
.without('a', 'b')
Такая схема содержит логическое противоречие.
Многие задачи проще описываются через:
.and()
.or()
.xor()
чем через сложные конструкции when().
Схема Joi должна проверять:
Сложные вычисления и бизнес-процессы лучше выносить отдельно.
Крупные условные конструкции удобно разбивать:
const credentialsSchema = Joi.object({
login: Joi.string(),
password: Joi.string()
});
const profileSchema = Joi.object({
firstName: Joi.string(),
lastName: Joi.string()
});
const passwordConfirmation = Joi.string()
.valid(Joi.ref('password'))
.required();
Большое количество вложенных when() может усложнять
выполнение валидации.
Особенно дорого обходятся:
custom() проверки.Для оптимизации:
| Подход | Назначение |
|---|---|
with() |
одно поле требует другое |
without() |
запрет совместного использования |
and() |
поля должны существовать вместе |
or() |
минимум одно поле |
xor() |
только одно поле |
when() |
условная схема |
ref() |
ссылки между значениями |
alternatives() |
альтернативные структуры |
custom() |
произвольная логика |