Условные схемы

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

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

Простейший пример зависимости:

const schema = Joi.object({
  role: Joi.string().valid('user', 'admin').required(),

  accessCode: Joi.string().when('role', {
    is: 'admin',
    then: Joi.required(),
    otherwise: Joi.optional()
  })
});

В этом случае поле accessCode становится обязательным только если role === 'admin'.

Ключевая идея заключается в разделении схемы на ветви:

  • is — условие сравнения
  • then — схема, применяемая при выполнении условия
  • otherwise — схема по умолчанию

Сравнение с несколькими значениями

Условие может быть расширено до набора значений:

const schema = Joi.object({
  status: Joi.string().valid('draft', 'published', 'archived'),

  publishedAt: Joi.date().when('status', {
    is: Joi.valid('published', 'archived'),
    then: Joi.required(),
    otherwise: Joi.forbidden()
  })
});

Здесь используется вложенный валидатор Joi.valid(), который позволяет проверять принадлежность к множеству.

Использование логики “если — иначе если — иначе”

Joi поддерживает цепочки условий через массив правил:

const schema = Joi.object({
  plan: Joi.string().valid('free', 'pro', 'enterprise'),

  storageLimit: Joi.number().when('plan', [
    {
      is: 'free',
      then: Joi.valid(5)
    },
    {
      is: 'pro',
      then: Joi.valid(50)
    },
    {
      is: 'enterprise',
      then: Joi.valid(500)
    }
  ])
});

Такая конструкция позволяет описывать множественные ветвления без вложенных конструкций.

Зависимости между полями объекта

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

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

  confirmPassword: Joi.string().when('password', {
    is: Joi.exist(),
    then: Joi.valid(Joi.ref('password')).required(),
    otherwise: Joi.forbidden()
  })
});

Здесь используется ссылка Joi.ref, позволяющая сравнивать значения разных полей.

Условная обязательность

Одно из наиболее частых применений — динамическое определение обязательности поля:

const schema = Joi.object({
  deliveryType: Joi.string().valid('courier', 'pickup'),

  address: Joi.string().when('deliveryType', {
    is: 'courier',
    then: Joi.required(),
    otherwise: Joi.optional()
  })
});

Логика обязательности становится частью схемы, а не внешней проверки.

Контекст нескольких зависимостей

Условия могут зависеть сразу от нескольких полей через switch-подобные конструкции:

const schema = Joi.object({
  userType: Joi.string().valid('guest', 'registered'),
  age: Joi.number(),

  discount: Joi.number().when(Joi.object({
    userType: Joi.valid('registered'),
    age: Joi.number().min(18)
  }).unknown(), {
    then: Joi.valid(10),
    otherwise: Joi.valid(0)
  })
});

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

Использование context и внешних параметров

Помимо внутренних полей объекта, Joi позволяет использовать внешние значения через контекст:

const schema = Joi.object({
  price: Joi.number().when('$currency', {
    is: 'KZT',
    then: Joi.max(100000),
    otherwise: Joi.max(1000)
  })
});

При валидации передаётся контекст:

schema.validate(data, { context: { currency: 'KZT' } });

Это расширяет область применения условных схем за пределы одного объекта.

Условные массивы и элементы

Условия применимы и к структурам внутри массивов:

const schema = Joi.object({
  users: Joi.array().items(
    Joi.object({
      type: Joi.string().valid('admin', 'user'),
      permissions: Joi.array().when('type', {
        is: 'admin',
        then: Joi.required(),
        otherwise: Joi.forbidden()
      })
    })
  )
});

Каждый элемент массива получает собственную условную логику.

Условные альтернативы через alternatives()

Помимо .when(), Joi предоставляет Joi.alternatives(), позволяющий описывать несколько возможных схем:

const schema = Joi.alternatives().conditional('type', [
  {
    is: 'email',
    then: Joi.string().email()
  },
  {
    is: 'phone',
    then: Joi.string().pattern(/^[0-9]+$/)
  }
]);

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

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

Условия могут быть вложенными, создавая многоуровневую логику:

const schema = Joi.object({
  subscription: Joi.string().valid('basic', 'pro'),

  features: Joi.object({
    analytics: Joi.boolean().when('subscription', {
      is: 'pro',
      then: Joi.valid(true),
      otherwise: Joi.valid(false)
    })
  })
});

Каждый уровень объекта может иметь собственные зависимости.

Комбинация условий и трансформаций

Условная схема может сочетаться с преобразованиями:

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

  permissions: Joi.array().when('role', {
    is: 'admin',
    then: Joi.array().items(Joi.string().uppercase()),
    otherwise: Joi.array().items(Joi.string().lowercase())
  })
});

Таким образом условие влияет не только на валидность, но и на обработку данных.

Особенности выполнения условий

При построении сложных схем важно учитывать порядок вычисления:

  • сначала разрешаются ссылки Joi.ref
  • затем вычисляются условия .when()
  • после этого применяется основная схема поля

Ошибки в логике условий часто возникают из-за неправильной интерпретации значений на момент валидации.

Ограничения и типовые ошибки

На практике условные схемы часто усложняются без необходимости, что приводит к:

  • циклическим зависимостям между полями
  • конфликтам required и forbidden
  • трудно читаемым вложенным условиям

Наиболее проблемные случаи связаны с одновременным использованием нескольких .when() на одном поле, где порядок правил начинает влиять на итоговую схему.

Композиция условных схем

Схемы можно выносить и комбинировать:

const baseEmail = Joi.string().email();

const schema = Joi.object({
  contactMethod: Joi.string().valid('email', 'sms'),

  contact: baseEmail.when('contactMethod', {
    is: 'email',
    then: Joi.required(),
    otherwise: Joi.forbidden()
  })
});

Это позволяет повторно использовать базовые определения и накладывать на них условия без дублирования логики.