Сложные условия

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


Условная логика через .when()

Основной инструмент построения зависимостей — метод .when(). Он позволяет изменять схему поля в зависимости от значения другого поля или собственного значения.

Зависимость от другого поля

const schema = Joi.object({
  type: Joi.string().valid('email', 'phone').required(),

  value: Joi.when('type', {
    is: 'email',
    then: Joi.string().email().required(),
    otherwise: Joi.string().pattern(/^\d{10,15}$/).required()
  })
});

Логика:

  • если type === 'email', поле value должно быть email
  • иначе — строка с телефоном

Условие с множественными вариантами switch

Для более сложных сценариев применяется массив условий:

const schema = Joi.object({
  method: Joi.string().required(),

  details: Joi.when('method', {
    switch: [
      {
        is: 'card',
        then: Joi.object({
          cardNumber: Joi.string().creditCard().required(),
          cvv: Joi.string().length(3).required()
        })
      },
      {
        is: 'paypal',
        then: Joi.object({
          email: Joi.string().email().required()
        })
      }
    ],
    otherwise: Joi.forbidden()
  })
});

Такой подход используется, когда набор структур сильно различается.


Использование контекста (context)

Условия могут зависеть не только от полей объекта, но и от внешнего контекста валидации:

const schema = Joi.object({
  price: Joi.number().when('$currency', {
    is: 'USD',
    then: Joi.number().min(1),
    otherwise: Joi.number().min(0.5)
  })
});

schema.validate({ price: 10 }, { context: { currency: 'USD' } });

Контекст позволяет внедрять глобальные параметры валидации, не включая их в сам объект.


Зависимости нескольких полей

.and(), .or(), .xor()

Эти методы задают логические зависимости между полями объекта.

.and() — все или ничего

Joi.object({
  a: Joi.number(),
  b: Joi.number()
}).and('a', 'b');

Если присутствует a, то обязателен b, и наоборот.


.or() — хотя бы одно поле

Joi.object({
  email: Joi.string().email(),
  phone: Joi.string()
}).or('email', 'phone');

Допустимо наличие одного или обоих полей.


.xor() — строго одно поле

Joi.object({
  username: Joi.string(),
  email: Joi.string().email()
}).xor('username', 'email');

Запрещает одновременное наличие обоих полей.


Взаимоисключающие поля .nand() и .with()

.nand() — нельзя использовать вместе

Joi.object({
  password: Joi.string(),
  token: Joi.string()
}).nand('password', 'token');

Одновременное присутствие запрещено.


.with() — обязательная связка

Joi.object({
  cardNumber: Joi.string(),
  cvv: Joi.string()
}).with('cardNumber', 'cvv');

Если есть cardNumber, требуется cvv.


Динамическая трансформация .alter()

Метод .alter() позволяет определять альтернативные схемы для разных сценариев использования.

const schema = Joi.object({
  password: Joi.string().min(6)
    .alter({
      register: (schema) => schema.min(8).required(),
      login: (schema) => schema.required()
    })
});

Применение:

schema.tailor('register').validate({ password: '12345678' });

Условные ограничения через .ruleset()

Позволяет комбинировать правила с переопределением логики:

const schema = Joi.number()
  .min(0)
  .max(100)
  .when('..level', {
    is: 'strict',
    then: Joi.number().min(10).max(90)
  });

Используется для наложения дополнительных ограничений поверх базовых.


Проверка наличия и обязательности .presence()

Условная обязательность управляется через .presence():

Joi.object({
  apiKey: Joi.string()
}).presence('optional');

Возможные значения:

  • required
  • optional
  • forbidden

Условия с использованием .ref()

Ссылки позволяют обращаться к другим полям объекта:

const schema = Joi.object({
  min: Joi.number().required(),
  max: Joi.number().greater(Joi.ref('min')).required()
});

Здесь max зависит от значения min.


Условные преобразования значений

Сложные схемы часто комбинируются с .custom():

Joi.number().custom((value, helpers) => {
  if (value < helpers.state.ancestors[0].min) {
    return helpers.error('number.min');
  }
  return value;
});

Это позволяет реализовывать произвольную бизнес-логику, выходящую за рамки стандартных условий.


Вложенные условия в объектах

Условные правила могут применяться на глубоко вложенных структурах:

const schema = Joi.object({
  user: Joi.object({
    role: Joi.string(),
    permissions: Joi.when('role', {
      is: 'admin',
      then: Joi.array().items(Joi.string()).min(1),
      otherwise: Joi.forbidden()
    })
  })
});

Зависимости могут распространяться по всей иерархии объекта.


Комбинирование условий

В реальных схемах часто объединяются несколько механизмов:

  • .when() для динамики
  • .xor() и .or() для логики присутствия
  • .ref() для межполевых сравнений
  • .context() для внешних параметров
const schema = Joi.object({
  mode: Joi.string().valid('create', 'update'),

  id: Joi.when('mode', {
    is: 'update',
    then: Joi.number().required(),
    otherwise: Joi.forbidden()
  }),

  email: Joi.string().email().when('mode', {
    is: 'create',
    then: Joi.required()
  })
});

Такие схемы формируют декларативное описание бизнес-логики на уровне данных, исключая необходимость ручных проверок в коде приложения.