Any и forbidden

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

Схема Joi.any() является базовым строительным блоком для всех остальных типов (string, number, object и т.д.), поскольку они наследуют общую логику валидации от него.

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

При использовании Joi.any():

  • допускаются значения любого типа (строки, числа, объекты, массивы, null, undefined)
  • отсутствует проверка структуры или формата
  • значение считается валидным практически всегда

Пример:

import Joi from 'joi';

const schema = Joi.any();

schema.validate(123);        // valid
schema.validate("text");     // valid
schema.validate({ a: 1 });   // valid
schema.validate(null);       // valid

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

Ограничения поверх any

Несмотря на «всеядность», any поддерживает систему модификаторов:

const schema = Joi.any()
  .valid('A', 'B')
  .required();

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

Также доступны:

  • allow() — расширение допустимых значений
  • invalid() — исключение значений
  • custom() — пользовательская логика проверки
  • rules() — внутренние расширения типов

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

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

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

const schema = Joi.object({
  metadata: Joi.any(),
  timestamp: Joi.date(),
});

Поле metadata может содержать произвольную структуру, не нарушая валидацию объекта.


Семантика отсутствия ограничений

Ключевая особенность any — отсутствие навязанных ограничений. Однако это не означает полное отсутствие логики проверки.

По умолчанию any:

  • разрешает любые типы
  • не нормализует данные
  • не выполняет преобразования
  • не валидирует структуру

Но при добавлении модификаторов поведение изменяется строго детерминированно.


Joi.forbidden()

Противоположным по смыслу инструментом является Joi.forbidden(). Эта схема обозначает абсолютный запрет на наличие поля или значения.

С технической точки зрения это специализированная форма any, настроенная на всегда невалидное состояние при наличии данных.

const schema = Joi.object({
  token: Joi.forbidden()
});

Любое присутствие token в объекте приведёт к ошибке валидации.

Поведение forbidden

  • поле не должно присутствовать
  • любое значение считается недопустимым
  • undefined также трактуется как отсутствие, но наличие ключа уже нарушает правило

Пример:

const schema = Joi.object({
  secret: Joi.forbidden()
});

schema.validate({}); 
// valid

schema.validate({ secret: '123' });
// error

Различие между any и forbidden

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

Схема Поведение
Joi.any() разрешает всё
Joi.forbidden() запрещает любое присутствие

Фактически:

  • any задаёт отсутствие ограничений
  • forbidden задаёт максимальное ограничение (полный запрет)

Внутреннее представление forbidden

Joi.forbidden() реализуется как частный случай any с модификатором, который всегда возвращает ошибку при наличии значения.

Эквивалентная конструкция:

Joi.any().forbidden()

Однако использование прямого Joi.forbidden() является более выразительным и семантически точным.


Сценарии применения any

1. Динамические структуры данных

В системах, где структура заранее неизвестна:

const schema = Joi.object({
  payload: Joi.any()
});

Поле payload может содержать результат стороннего API или произвольные данные.


2. Проксирование данных

При передаче данных без изменений:

const schema = Joi.object({
  data: Joi.any().required()
});

Используется как «прозрачный контейнер».


3. Совместимость с внешними источниками

Когда формат не контролируется:

  • webhook-события
  • события очередей сообщений
  • JSON от сторонних сервисов

Сценарии применения forbidden

1. Запрет служебных полей

const schema = Joi.object({
  password: Joi.string().required(),
  _internal: Joi.forbidden()
});

Поле _internal исключается из входных данных независимо от его наличия.


2. Защита API контрактов

При строгом контроле входных данных:

const schema = Joi.object({
  role: Joi.string(),
  adminOverride: Joi.forbidden()
});

Любые попытки передать adminOverride блокируются на уровне валидации.


3. Исключение legacy полей

При эволюции API:

const schema = Joi.object({
  newField: Joi.string(),
  oldField: Joi.forbidden()
});

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


Поведение при вложенных структурах

any внутри объектов

const schema = Joi.object({
  config: Joi.object({
    raw: Joi.any()
  })
});

Поле raw допускает любые значения, но структура config остаётся ограниченной.


forbidden внутри объектов

const schema = Joi.object({
  config: Joi.object({
    debug: Joi.forbidden()
  })
});

Даже наличие ключа debug нарушает схему объекта config.


Совместимость с allow и strip

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

allow()

Joi.any().allow(null)

Позволяет явно расширить допустимые значения.

strip()

Joi.any().strip()

Удаляет поле из результата после валидации, даже если оно допустимо.

В сочетании:

Joi.any().allow('x').strip()

значение может быть валидным, но будет исключено из результата.


Ошибки валидации forbidden

При нарушении forbidden возвращается ошибка с типом:

  • any.unknown (в зависимости от конфигурации Joi)
  • или специализированное сообщение о запрещённом поле

Пример структуры ошибки:

{
  "message": "\"secret\" is not allowed",
  "path": ["secret"],
  "type": "object.unknown"
}

Особенности поведения при отсутствии ключа

Важно различие:

  • отсутствие ключа → валидно
  • ключ с undefined → может считаться нарушением в зависимости от настройки presence
const schema = Joi.object({
  token: Joi.forbidden()
});

schema.validate({});
// valid

Роль any как базового типа

any играет ключевую архитектурную роль:

  • является корнем иерархии типов
  • определяет базовые методы (required, optional, valid, invalid)
  • используется как fallback при расширении схем

Все специализированные типы в Joi в конечном счёте опираются на его поведение.


Сравнение поведения в цепочках

Joi.any().valid(1, 2)
Joi.any().invalid(3)
Joi.any().required()
Joi.any().forbidden()

Каждый метод модифицирует базовую универсальность any, но forbidden полностью блокирует допустимость независимо от других правил.

При конфликте правил:

  • forbidden имеет приоритет
  • остальные модификаторы игнорируются в контексте присутствия значения

Практическая модель восприятия

Логически поведение можно интерпретировать так:

  • any → «нет ограничений»
  • forbidden → «полное отрицание присутствия»

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