Валидация объектов редко ограничивается проверкой типов и обязательности отдельных полей. Во многих случаях требуется описывать связи между ключами:
Для подобных сценариев в Joi используются методы:
and()or()xor()oxor()nand()with()without()Эти методы работают на уровне объекта и описывают взаимосвязи между полями схемы.
and()Метод and() требует совместного присутствия всех
перечисленных ключей.
Если один из ключей существует, остальные тоже обязаны присутствовать.
Joi.object({
a: Joi.any(),
b: Joi.any()
}).and('a', 'b')
const Joi = require('joi');
const schema = Joi.object({
login: Joi.string(),
password: Joi.string()
}).and('login', 'password');
{
login: 'admin',
password: '123456'
}
{}
{
login: 'admin'
}
Ошибка:
"value" contains [login] without its required peers [password]
and()Метод работает по следующему правилу:
| login | password | Результат |
|---|---|---|
| нет | нет | валидно |
| есть | есть | валидно |
| есть | нет | ошибка |
| нет | есть | ошибка |
Координаты часто должны передаваться одновременно.
const schema = Joi.object({
lat: Joi.number(),
lng: Joi.number()
}).and('lat', 'lng');
{
lat: 51.1694,
lng: 71.4491
}
{}
{
lat: 51.1694
}
or()Метод or() требует наличия хотя бы одного ключа из
перечисленных.
object.or('a', 'b', 'c')
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'
}
{}
Ошибка:
"value" must contain at least one of [email, phone]
or()| phone | Результат | |
|---|---|---|
| нет | нет | ошибка |
| есть | нет | валидно |
| нет | есть | валидно |
| есть | есть | валидно |
xor()xor() требует наличия только одного ключа из группы.
Если присутствуют сразу несколько ключей — возникает ошибка.
object.xor('a', 'b')
const schema = Joi.object({
password: Joi.string(),
oauthToken: Joi.string()
}).xor('password', 'oauthToken');
{
password: '123456'
}
{
oauthToken: 'token123'
}
{}
Ошибка:
"value" must contain at least one of [password, oauthToken]
{
password: '123456',
oauthToken: 'token123'
}
Ошибка:
"value" contains a conflict between exclusive peers [password, oauthToken]
xor()| password | oauthToken | Результат |
|---|---|---|
| нет | нет | ошибка |
| есть | нет | валидно |
| нет | есть | валидно |
| есть | есть | ошибка |
or() от
xor()or()Требует минимум одно поле.
Допускает наличие нескольких.
.or('email', 'phone')
xor()Требует строго одно поле.
Несколько полей запрещены.
.xor('password', 'oauthToken')
oxor()oxor() — optional xor.
Разрешает отсутствие всех полей, но если одно поле присутствует — остальные запрещены.
const schema = Joi.object({
telegram: Joi.string(),
whatsapp: Joi.string()
}).oxor('telegram', 'whatsapp');
oxor()| telegram | Результат | |
|---|---|---|
| нет | нет | валидно |
| есть | нет | валидно |
| нет | есть | валидно |
| есть | есть | ошибка |
xor() от
oxor()xor()Требует наличие одного поля обязательно.
.xor('a', 'b')
oxor()Все поля могут отсутствовать.
.oxor('a', 'b')
nand()nand() запрещает совместное присутствие указанных
ключей.
object.nand('a', 'b')
const schema = Joi.object({
fixedPrice: Joi.number(),
discountPercent: Joi.number()
}).nand('fixedPrice', 'discountPercent');
{
fixedPrice: 1000
}
{
discountPercent: 15
}
{}
{
fixedPrice: 1000,
discountPercent: 15
}
Ошибка:
"fixedPrice" must not exist simultaneously with [discountPercent]
nand()| fixedPrice | discountPercent | Результат |
|---|---|---|
| нет | нет | валидно |
| есть | нет | валидно |
| нет | есть | валидно |
| есть | есть | ошибка |
with()with() требует наличие зависимого ключа при
существовании основного.
object.with('a', 'b')
const schema = Joi.object({
token: Joi.string(),
deviceId: Joi.string()
}).with('token', 'deviceId');
with()| token | deviceId | Результат |
|---|---|---|
| нет | нет | валидно |
| есть | есть | валидно |
| нет | есть | валидно |
| есть | нет | ошибка |
with() от
and()and()Оба поля зависят друг от друга взаимно.
.and('a', 'b')
Если есть a, нужен b.
Если есть b, нужен a.
with()Зависимость односторонняя.
.with('a', 'b')
Если есть a, нужен b.
Но b может существовать отдельно.
without()without() запрещает наличие зависимого ключа при
существовании основного.
object.without('a', 'b')
const schema = Joi.object({
guest: Joi.boolean(),
password: Joi.string()
}).without('guest', 'password');
without()| guest | password | Результат |
|---|---|---|
| нет | нет | валидно |
| есть | нет | валидно |
| нет | есть | валидно |
| есть | есть | ошибка |
const Joi = require('joi');
const schema = Joi.object({
email: Joi.string().email(),
phone: Joi.string(),
password: Joi.string(),
oauthToken: Joi.string(),
lat: Joi.number(),
lng: Joi.number()
})
.or('email', 'phone')
.xor('password', 'oauthToken')
.and('lat', 'lng');
.or('email', 'phone')
Требует минимум один способ связи.
.xor('password', 'oauthToken')
Разрешает только один способ аутентификации.
.and('lat', 'lng')
Требует передачу обеих координат одновременно.
const result = schema.validate({
email: 'user@mail.com',
password: '123456',
lat: 50,
lng: 70
});
console.log(result.error);
Все методы зависимостей поддерживают кастомизацию ошибок через
messages().
const schema = Joi.object({
password: Joi.string(),
oauthToken: Joi.string()
})
.xor('password', 'oauthToken')
.messages({
'object.xor': 'Должен использоваться только один способ авторизации'
});
| Код | Описание |
|---|---|
| object.and | отсутствуют связанные поля |
| object.or | отсутствует хотя бы одно поле |
| object.xor | конфликт эксклюзивных полей |
| object.oxor | конфликт optional xor |
| object.nand | запрещённая комбинация |
| object.with | отсутствует зависимое поле |
| object.without | запрещённое зависимое поле |
required()Методы зависимостей работают независимо от
required().
const schema = Joi.object({
email: Joi.string().required(),
phone: Joi.string()
}).or('email', 'phone');
Здесь or() становится бессмысленным, потому что
email уже обязателен.
xor().xor('a', 'b')
Это не означает:
«одно из полей желательно»
Это означает:
«ровно одно поле обязательно»
{
email: ''
}
Joi считает пустую строку существующим значением.
Для корректной обработки часто используется:
Joi.string().empty('')
const schema = Joi.object({
email: Joi.string().email().empty(''),
phone: Joi.string().empty('')
}).or('email', 'phone');
Методы работают и во вложенных схемах.
const schema = Joi.object({
profile: Joi.object({
firstName: Joi.string(),
lastName: Joi.string()
}).and('firstName', 'lastName')
});
Допускается цепочка из нескольких правил.
const schema = Joi.object({
username: Joi.string(),
email: Joi.string(),
password: Joi.string(),
repeatPassword: Joi.string(),
apiKey: Joi.string()
})
.or('username', 'email')
.and('password', 'repeatPassword')
.xor('password', 'apiKey');
.or('username', 'email')
Нужен минимум один идентификатор.
.and('password', 'repeatPassword')
Пароли должны приходить вместе.
.xor('password', 'apiKey')
Разрешён либо пароль, либо API-ключ.
| Метод | Смысл |
|---|---|
| and | все поля вместе |
| or | минимум одно поле |
| xor | строго одно поле |
| oxor | максимум одно поле |
| nand | нельзя вместе |
| with | при наличии A нужен B |
| without | при наличии A запрещён B |