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

Валидация данных в схемах Joi строится на различии между обязательными и необязательными полями. Классическое использование .required() фиксирует жёсткое требование наличия значения, тогда как .optional() допускает отсутствие поля без ошибок. Однако в реальных сценариях структура данных редко остаётся статичной: обязательность часто зависит от других значений внутри объекта.

Условная обязательность означает, что наличие или отсутствие поля определяется контекстом — значениями других полей, внешними параметрами или логическими связками внутри схемы. В Joi для этого используется набор механизмов, позволяющих описывать динамические правила в декларативной форме.

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


Метод when: зависимость от других полей

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

Базовый синтаксис:

Joi.object({
  type: Joi.string().valid('email', 'phone'),

  email: Joi.string().email().when('type', {
    is: 'email',
    then: Joi.required(),
    otherwise: Joi.optional()
  }),

  phone: Joi.string().when('type', {
    is: 'phone',
    then: Joi.required(),
    otherwise: Joi.optional()
  })
});

В этом примере обязательность email и phone зависит от значения поля type. Логика становится декларативной: правило описывает не процесс проверки, а условия его применения.

Использование нескольких условий

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

Joi.object({
  role: Joi.string(),
  accessLevel: Joi.number(),

  secretKey: Joi.string().when('role', {
    is: 'admin',
    then: Joi.when('accessLevel', {
      is: Joi.number().min(5),
      then: Joi.required(),
      otherwise: Joi.optional()
    }),
    otherwise: Joi.forbidden()
  })
});

Здесь обязательность зависит сразу от двух факторов: роли и уровня доступа.


Условные зависимости через ref

В Joi возможно ссылаться на значения других полей через Joi.ref. Это особенно важно при сравнении значений или вычислении условий на основе динамических данных.

Joi.object({
  password: Joi.string().required(),
  confirmPassword: Joi.string().valid(Joi.ref('password')).required()
});

Хотя этот пример не изменяет обязательность напрямую, ref часто используется внутри when:

Joi.object({
  minAge: Joi.number(),
  maxAge: Joi.number().when('minAge', {
    is: Joi.number().greater(18),
    then: Joi.required(),
    otherwise: Joi.optional()
  })
});

Значение minAge влияет на обязательность maxAge, создавая зависимость между полями.


Альтернативные схемы и alternatives

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

Joi.object({
  contact: Joi.alternatives().conditional('preferred', {
    is: 'email',
    then: Joi.string().email().required(),
    otherwise: Joi.string().pattern(/^\+?[0-9]+$/).required()
  }),

  preferred: Joi.string().valid('email', 'phone').required()
});

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


Декларативные зависимости: or, and, xor, with, without

Помимо when, Joi предоставляет набор логических операторов, описывающих зависимости между полями.

or

Обязательное наличие хотя бы одного поля:

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

and

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

Joi.object({
  street: Joi.string(),
  city: Joi.string(),
  zip: Joi.string()
}).and('street', 'city', 'zip');

xor

Только одно из полей должно быть указано:

Joi.object({
  passport: Joi.string(),
  driverLicense: Joi.string()
}).xor('passport', 'driverLicense');

with

Если одно поле присутствует, другое становится обязательным:

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

without

Поля взаимоисключают друг друга:

Joi.object({
  internalId: Joi.string(),
  publicId: Joi.string()
}).without('internalId', 'publicId');

Эти конструкции формируют логическую систему ограничений, дополняя условную обязательность более строгими связями.


Контекстная валидация и external context

Joi позволяет учитывать внешний контекст при валидации через передачу context в метод .validate().

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

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

Контекстные переменные ($currency) дают возможность отделять данные объекта от внешних условий, сохраняя чистоту схемы.


Комбинации условий и сложные правила

В реальных приложениях условная обязательность часто строится из нескольких уровней логики. Joi позволяет комбинировать when, ref, логические операторы и альтернативы.

Joi.object({
  userType: Joi.string().valid('guest', 'registered', 'admin'),

  email: Joi.string().email().when('userType', {
    switch: [
      {
        is: 'guest',
        then: Joi.required()
      },
      {
        is: 'registered',
        then: Joi.required()
      }
    ],
    otherwise: Joi.forbidden()
  }),

  adminCode: Joi.string().when('userType', {
    is: 'admin',
    then: Joi.when('email', {
      is: Joi.exist(),
      then: Joi.required(),
      otherwise: Joi.forbidden()
    })
  })
});

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


Типовые паттерны

Зависимая обязательность по типу сущности

Joi.object({
  entityType: Joi.string().valid('person', 'company'),

  firstName: Joi.string().when('entityType', {
    is: 'person',
    then: Joi.required(),
    otherwise: Joi.forbidden()
  }),

  companyName: Joi.string().when('entityType', {
    is: 'company',
    then: Joi.required(),
    otherwise: Joi.forbidden()
  })
});

Условная обязательность по наличию значения

Joi.object({
  hasDiscount: Joi.boolean(),

  discountValue: Joi.number().when('hasDiscount', {
    is: true,
    then: Joi.required().min(1),
    otherwise: Joi.optional()
  })
});

Взаимосвязанные поля с частичной обязательностью

Joi.object({
  startDate: Joi.date(),
  endDate: Joi.date().when('startDate', {
    is: Joi.exist(),
    then: Joi.required().greater(Joi.ref('startDate')),
    otherwise: Joi.optional()
  })
});

Многоуровневая логика и вложенные условия

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

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

  paymentMethod: Joi.string().when('subscription', {
    is: 'pro',
    then: Joi.when('billingCycle', {
      is: 'monthly',
      then: Joi.required(),
      otherwise: Joi.optional()
    })
  }),

  billingCycle: Joi.string().valid('monthly', 'yearly')
});

Такая структура позволяет формировать зависимости, близкие к бизнес-правилам предметной области.


Обобщённые стратегии построения условной обязательности

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

  • привязка к значению другого поля через when
  • использование ссылок ref для динамических сравнений
  • логические зависимости через or, and, xor
  • альтернативные схемы через alternatives
  • внешние параметры через context
  • комбинирование нескольких условий в одном узле схемы

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