Альтернативы: alternatives()

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


Базовая идея alternatives

alternatives() описывает значение, которое может соответствовать одной из нескольких схем. Это полезно в ситуациях, когда формат данных не фиксирован:

  • строка или число
  • объект разных типов
  • разные версии API-ответа
  • условно изменяемая структура

Синтаксис alternatives()

Статическое объявление

const Joi = require('joi');

const schema = Joi.alternatives().try(
  Joi.string(),
  Joi.number()
);

В этом случае значение считается валидным, если оно является строкой или числом.


Короткая форма

const schema = Joi.alternatives(
  Joi.string(),
  Joi.number()
);

Обе формы эквивалентны, но .try() делает намерение более явным.


Поведение по умолчанию

По умолчанию alternatives() работает в режиме:

any — достаточно, чтобы совпала хотя бы одна схема.

const schema = Joi.alternatives().try(
  Joi.string().min(3),
  Joi.number().integer()
);

schema.validate('abc'); // valid
schema.validate(123);   // valid
schema.validate('a');   // invalid

Режим match: управление стратегией проверки

Метод match() позволяет изменить логику сопоставления альтернатив.

match(‘any’) — стандартное поведение

Достаточно совпадения с одной схемой.

Joi.alternatives().match('any').try(
  Joi.string(),
  Joi.number()
);

match(‘one’) — строго одна схема

Значение должно соответствовать только одной из альтернатив.

const schema = Joi.alternatives().match('one').try(
  Joi.string().max(5),
  Joi.string().alphanum()
);

Если строка подходит сразу под обе схемы, будет ошибка.


match(‘all’) — соответствие всем схемам

Редкий режим, при котором значение должно удовлетворять каждой схеме.

const schema = Joi.alternatives().match('all').try(
  Joi.string().min(3),
  Joi.string().max(10)
);

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


alternatives() с объектами

alternatives() часто применяется для описания разных форм объекта.

const schema = Joi.alternatives().try(
  Joi.object({
    type: Joi.string().valid('A').required(),
    value: Joi.number().required()
  }),
  Joi.object({
    type: Joi.string().valid('B').required(),
    value: Joi.string().required()
  })
);

Здесь структура зависит от значения поля type.


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

Хотя alternatives() используется напрямую, чаще применяется вместе с when().

const schema = Joi.object({
  type: Joi.string().required(),
  value: Joi.alternatives().conditional('type', {
    is: 'number',
    then: Joi.number(),
    otherwise: Joi.string()
  })
});

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


Динамическое ветвление логики

const schema = Joi.alternatives().conditional('mode', [
  {
    is: 'strict',
    then: Joi.object({
      data: Joi.string().required()
    })
  },
  {
    is: 'loose',
    then: Joi.object({
      data: Joi.any()
    })
  }
]);

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


Работа с массивами альтернатив

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

const schema = Joi.array().items(
  Joi.alternatives().try(
    Joi.string(),
    Joi.number(),
    Joi.object({ id: Joi.number() })
  )
);

Массив может содержать элементы разных типов.


Вложенные альтернативы

const schema = Joi.object({
  payload: Joi.alternatives().try(
    Joi.object({
      kind: Joi.string().valid('text'),
      value: Joi.string()
    }),
    Joi.object({
      kind: Joi.string().valid('binary'),
      value: Joi.binary()
    })
  )
});

Ошибки при использовании alternatives()

Конфликтующие схемы

Joi.alternatives().match('one').try(
  Joi.number().integer(),
  Joi.number().min(0)
);

Число 5 удовлетворяет обеим схемам → ошибка.


Слишком широкие условия

Joi.alternatives().try(
  Joi.any(),
  Joi.string()
);

Первая схема перекрывает вторую, делая её бессмысленной.


Производительность alternatives()

Использование alternatives() увеличивает стоимость проверки, поскольку:

  • каждая схема проверяется последовательно
  • возможен ранний выход при match(‘any’)
  • match(‘all’) всегда требует полной проверки

Оптимизация достигается за счёт:

  • упорядочивания более вероятных схем первыми
  • избегания перекрывающихся условий
  • использования when() вместо больших try-цепочек

Практические сценарии применения

Разные версии API

const schema = Joi.alternatives().try(
  Joi.object({
    version: Joi.string().valid('1'),
    data: Joi.string()
  }),
  Joi.object({
    version: Joi.string().valid('2'),
    payload: Joi.object()
  })
);

Полиморфные ответы сервера

const responseSchema = Joi.alternatives().try(
  Joi.object({ success: Joi.boolean().valid(true) }),
  Joi.object({ success: Joi.boolean().valid(false), error: Joi.string() })
);

Пользовательский ввод

const inputSchema = Joi.alternatives().try(
  Joi.string().email(),
  Joi.string().pattern(/^\+\d{10,15}$/)
);

Ограничения alternatives()

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