Has и single

Валидация структуры объектов в Joi часто требует не только проверки типов и форматов, но и контроля за наличием ключей в зависимости от контекста данных. Механизм has позволяет задавать обязательное присутствие определённых свойств объекта, а single управляет интерпретацией одиночных значений как массивов, что особенно важно при работе с API, где входные данные могут быть непоследовательными.


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

Базовое использование одного ключа

const schema = Joi.object({
  name: Joi.string(),
  age: Joi.number()
}).has('name');

В данном случае объект обязан содержать ключ name, независимо от того, присутствует ли age. Если name отсутствует, валидация завершится ошибкой.


Проверка нескольких ключей

Метод поддерживает передачу массива ключей, что позволяет задавать требования к группе полей:

const schema = Joi.object({
  username: Joi.string(),
  password: Joi.string(),
  email: Joi.string()
}).has(['username', 'password']);

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


Валидация значений ключей

has может принимать не только имя ключа, но и схему валидации, которая применяется к значению этого ключа:

const schema = Joi.object({
  role: Joi.string(),
  permissions: Joi.array()
}).has('permissions', Joi.array().min(1));

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


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

has не заменяет required, а дополняет его. Поля могут оставаться опциональными, но их совместное присутствие регулируется логикой схемы.

const schema = Joi.object({
  a: Joi.string(),
  b: Joi.string(),
  c: Joi.string()
}).has('a').has('b');

В этом случае объект обязан содержать как минимум a и b, тогда как c остаётся необязательным.


Поведение single в массивах

Метод single применяется к схемам массивов и изменяет поведение валидации таким образом, что одиночное значение автоматически трактуется как массив с одним элементом. Это решает распространённую проблему API, где одно и то же поле может приходить как строка или как массив строк.

Базовая концепция

const schema = Joi.array().items(Joi.string()).single();

Такое определение означает:

  • допустим массив строк;
  • допустима одиночная строка;
  • одиночная строка будет преобразована в массив из одного элемента.

Пример входных данных

Схема:

const schema = Joi.array().items(Joi.number()).single();

Валидные значения:

[1, 2, 3]  // остаётся массивом
5  // преобразуется в [5]

Работа с неоднородными входами

При интеграции с внешними API часто встречается смешанный формат данных:

  • иногда приходит "tag": "news"
  • иногда "tag": ["news", "sports"]

С single это обрабатывается единообразно:

const schema = Joi.array().items(Joi.string()).single();

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


Взаимодействие с items и строгой типизацией

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

const schema = Joi.array()
  .items(Joi.object({
    id: Joi.number().required()
  }))
  .single();

Одиночный объект будет преобразован в массив, но структура объекта всё равно проверяется в соответствии с items.


Совместное использование has и single в комплексных схемах

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

const schema = Joi.object({
  user: Joi.object({
    name: Joi.string().required(),
    roles: Joi.array().items(Joi.string()).single()
  }).has('name')
});

В этом примере:

  • объект user обязан содержать name;
  • поле roles может быть строкой или массивом строк;
  • в любом случае roles приводится к массиву.

Особенности преобразования данных

По умолчанию Joi выполняет преобразования входных данных (convert: true). Это напрямую влияет на поведение single, так как приведение одиночного значения к массиву происходит на этапе преобразования.

При отключённом преобразовании:

Joi.array().single().prefs({ convert: false })

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


Пограничные случаи и нюансы поведения

Отсутствие ключей при has

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


Пустые массивы и single

single не предотвращает появление пустого массива:

Joi.array().items(Joi.string()).single()

значение [] остаётся допустимым, если не заданы дополнительные ограничения (min, required, length).


Влияние порядка применения методов

В схемах массивов порядок цепочки важен:

Joi.array().single().items(Joi.string())

и

Joi.array().items(Joi.string()).single()

в большинстве случаев эквивалентны, но при сложных кастомных правилах и расширениях поведение может различаться из-за этапов обработки (конвертация, затем проверка элементов).


Использование в API-валидации и бизнес-логике

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

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


Поведение в контексте вложенных объектов

При вложенных структурах has действует локально для каждого объекта:

const schema = Joi.object({
  profile: Joi.object({
    firstName: Joi.string(),
    lastName: Joi.string()
  }).has('firstName')
});

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


Обработка ошибок валидации

Ошибки, возникающие при нарушении has, обычно связаны с отсутствием ключа и описываются как требования обязательного свойства объекта.

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