Invalid и disallow

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

Метод invalid() задаёт значения, которые считаются недопустимыми для конкретного поля. При совпадении входного значения с любым из перечисленных, валидация завершается ошибкой.

Базовая форма

const Joi = require('joi');

const schema = Joi.string().invalid('admin', 'root', 'superuser');

schema.validate('admin');
// → ошибка валидации

schema.validate('user');
// → валидно

Семантически invalid() расширяет ограничение схемы в отрицательную сторону: вместо перечисления разрешённых значений задаётся список запрещённых.

Использование с различными типами

Запрещённые значения могут применяться не только к строкам, но и ко всем поддерживаемым типам:

const schema = Joi.number().invalid(0, -1, 999);

schema.validate(0);   // ошибка
schema.validate(10);  // ок
const schema = Joi.boolean().invalid(false);

schema.validate(false); // ошибка
schema.validate(true);  // ок

Поведение при совпадении типов

Сравнение выполняется строго, без приведения типов:

const schema = Joi.number().invalid('5');

schema.validate(5);
// ошибка, так как '5' (строка) !== 5 (число)

Это поведение критично при работе с входными данными API, где типизация играет роль дополнительного слоя защиты.

Сочетание с allow и valid

invalid() логически противоположен valid(). При одновременном использовании действует принцип приоритета запрета: если значение попадает в список запрещённых, оно будет отклонено даже при наличии в списке допустимых.

const schema = Joi.number()
  .valid(1, 2, 3, 4, 5)
  .invalid(3);

schema.validate(3); // ошибка
schema.validate(2); // ок

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

Множественные вызовы invalid

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

const schema = Joi.string()
  .invalid('a')
  .invalid('b', 'c');

schema.validate('b'); // ошибка

Внутри библиотеки все значения агрегируются в единый набор запрещённых значений.

Ошибки валидации и их структура

При нарушении ограничения invalid() возвращается стандартный объект ошибки Joi:

const result = schema.validate('admin');

console.log(result.error.details);

Типичная структура содержит:

  • type: идентификатор ошибки (например, any.invalid)
  • message: человекочитаемое описание
  • context.value: значение, вызвавшее ошибку

Пример:

any.invalid: "value" contains an invalid value

Отличие invalid от custom validation

Логика invalid() декларативна и выполняется до пользовательских функций .custom():

const schema = Joi.string()
  .invalid('test')
  .custom((value, helpers) => {
    return value.toUpperCase();
  });

Если значение равно 'test', выполнение custom-функции не происходит, поскольку валидация завершается на этапе проверки invalid().

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

Метод disallow и его статус

Метод disallow() исторически использовался как синоним invalid(), однако в современных версиях Joi он считается устаревшим (deprecated).

const schema = Joi.string().disallow('admin');

Фактически он полностью эквивалентен:

const schema = Joi.string().invalid('admin');

Причины устаревания

Основная причина отказа от disallow() заключается в унификации API:

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

Использование disallow() в новых проектах считается нежелательным, хотя во многих кодовых базах он сохраняется ради обратной совместимости.

Сравнение invalid и disallow

Функционально различий нет, но есть различия в поддержке и семантике:

Метод Статус Рекомендуется Назначение
invalid() актуальный да явное запрещение
disallow() устаревший нет исторический синоним

Использование с объектами и ссылками

При работе со сложными структурами важно учитывать, что invalid() сравнивает значения по ссылке, а не по глубокой эквивалентности:

const schema = Joi.object({
  role: Joi.string().invalid('admin')
});

schema.validate({ role: 'admin' }); // ошибка

Для объектов:

const forbidden = { type: 'admin' };

const schema = Joi.object().invalid(forbidden);

schema.validate(forbidden); // ошибка (та же ссылка)
schema.validate({ type: 'admin' }); // может быть валидно

Таким образом, для структурных сравнений требуется отдельная логика через .custom().

Поведение при undefined и null

invalid() не блокирует undefined и null, если они не указаны явно:

const schema = Joi.string().invalid('admin');

schema.validate(null);     // валидно
schema.validate(undefined); // валидно

Чтобы запретить такие значения, используются дополнительные методы:

const schema = Joi.string()
  .invalid('admin')
  .required();

или:

Joi.string().invalid('admin', null, undefined);

Типичные сценарии применения

Запрет зарезервированных слов

const username = Joi.string().invalid(
  'admin',
  'root',
  'system'
);

Ограничение бизнес-правил

const orderStatus = Joi.string().invalid('deleted');

Защита системных значений

const role = Joi.string().invalid('superadmin');

Особенности объединения с альтернативами

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

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

schema.validate('test'); // ошибка

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

Итоговое поведение механизма исключений

Механизм invalid() в Joi представляет собой декларативный способ задания запрещённых значений, работающий на раннем этапе валидации и имеющий приоритет над другими правилами схемы. Устаревший disallow() сохраняется только для обратной совместимости и не должен использоваться в новых схемах.