And, or, xor для ключей

Валидация объектов редко ограничивается проверкой типов и обязательности отдельных полей. Во многих случаях требуется описывать связи между ключами:

  • один ключ обязателен только при наличии другого;
  • должно существовать хотя бы одно поле из группы;
  • поля не могут существовать одновременно;
  • разрешена только одна альтернатива;
  • группа ключей должна появляться совместно.

Для подобных сценариев в 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')

Пример: email или телефон

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()

email phone Результат
нет нет ошибка
есть нет валидно
нет есть валидно
есть есть валидно

Метод xor()

Назначение

xor() требует наличия только одного ключа из группы.

Если присутствуют сразу несколько ключей — возникает ошибка.


Синтаксис

object.xor('a', 'b')

Пример: пароль или OAuth

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 whatsapp Результат
нет нет валидно
есть нет валидно
нет есть валидно
есть есть ошибка

Отличие 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')

Пример: token и deviceId

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')

Пример: guest и password

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