Custom валидаторы

Библиотека Joi предоставляет механизм расширения стандартной системы валидации через пользовательские правила, позволяя внедрять сложную бизнес-логику, которая не покрывается встроенными типами и методами. Основной инструмент для этого — custom(), а также более глубокий механизм расширений через Joi.extend().


Использование кастомной логики становится необходимым в ситуациях, когда проверка данных зависит от внешних условий, комплексных вычислений или нестандартных форматов. В отличие от декларативных правил (min, max, email, pattern), пользовательские валидаторы позволяют описывать произвольное поведение валидации в виде функции.


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

import Joi fr om 'joi';

const schema = Joi.object({
  username: Joi.string().custom((value, helpers) => {
    if (value.includes('admin')) {
      return helpers.error('string.invalidUsername');
    }
    return value;
  })
});

В этом примере проверяется наличие запрещённой подстроки. При нарушении правила возвращается ошибка через helpers.error().


Контекст выполнения и helpers

Функция кастомной валидации получает второй аргумент — объект helpers, предоставляющий доступ к вспомогательным методам:

  • helpers.error(code, context) — генерация ошибки
  • helpers.state — доступ к состоянию валидации
  • helpers.prefs — пользовательские настройки схемы
  • helpers.original — исходное значение до преобразований
Joi.string().custom((value, helpers) => {
  const original = helpers.original;

  if (original !== value.trim()) {
    return helpers.error('string.whitespace');
  }

  return value;
});

Использование helpers.original позволяет сравнивать исходные данные с преобразованными, что полезно при цепочках trim(), lowercase() и других модификаторах.


Возврат значений и трансформация данных

Кастомные валидаторы могут не только проверять, но и изменять данные. Возврат нового значения считается допустимым поведением.

Joi.number().custom((value, helpers) => {
  return value * 2;
});

Здесь входное число трансформируется перед передачей дальше по схеме.

При необходимости можно сохранить исходную семантику и возвращать объект с метаданными:

Joi.string().custom((value, helpers) => {
  return {
    raw: value,
    length: value.length
  };
});

Асинхронная валидация

Кастомные валидаторы поддерживают асинхронную логику. Это важно при проверке данных через внешние сервисы или базы данных.

Joi.string().custom(async (value, helpers) => {
  const exists = await checkUserInDatabase(value);

  if (exists) {
    return helpers.error('string.alreadyExists');
  }

  return value;
});

Асинхронный валидатор автоматически переводит схему в режим Promise.

const result = await schema.validateAsync(data);

Обработка ошибок

Ошибки формируются через helpers.error(), где указывается код и дополнительные параметры контекста.

Joi.number().custom((value, helpers) => {
  if (value < 0) {
    return helpers.error('number.negative', { lim it: 0 });
  }
  return value;
});

Код ошибки затем может быть обработан через кастомные сообщения:

const schema = Joi.number().messages({
  'number.negative': 'Число не может быть меньше {#limit}'
});

Плейсхолдеры ({#limit}) берутся из контекста ошибки.


Многоступенчатая валидация

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

Joi.string()
  .min(5)
  .custom((value, helpers) => {
    if (value === '12345') {
      return helpers.error('string.weakPassword');
    }
    return value;
  })
  .max(20);

Сначала применяются встроенные правила, затем кастомная логика, после чего снова возможны встроенные проверки в зависимости от преобразований.


Расширение через Joi.extend()

Более системный способ добавления кастомной логики — создание новых типов через extend(). Этот подход используется для повторного применения правил в разных схемах.

const customJoi = Joi.extend((joi) => ({
  type: 'positiveString',
  base: joi.string(),
  validate(value, helpers) {
    if (value.includes('-')) {
      return { value, errors: helpers.error('string.noNegative') };
    }
  }
}));

Использование нового типа:

customJoi.object({
  amount: customJoi.positiveString()
});

Правила (rules) внутри расширений

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

const extendedJoi = Joi.extend((joi) => ({
  type: 'identifier',
  base: joi.string(),
  rules: {
    alphanumericOnly: {
      validate(value, helpers) {
        if (!/^[a-z0-9]+$/i.test(value)) {
          return helpers.error('string.alphanumeric');
        }
        return value;
      }
    }
  }
}));

Использование:

extendedJoi.identifier().alphanumericOnly();

Контекст и зависимости между полями

Кастомные валидаторы могут учитывать значения других полей через state.ancestors или ref().

Joi.object({
  password: Joi.string(),
  confirm: Joi.string().custom((value, helpers) => {
    const { password } = helpers.state.ancestors[0];

    if (value !== password) {
      return helpers.error('any.invalid');
    }

    return value;
  })
});

Также используется Joi.ref() для декларативных сравнений:

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

Состояние и порядок выполнения

helpers.state содержит информацию о текущем пути в объекте, что важно при работе с вложенными структурами.

Joi.object({
  user: Joi.object({
    age: Joi.number().custom((value, helpers) => {
      const path = helpers.state.path;
      return value;
    })
  })
});

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


Практические паттерны кастомной валидации

Часто используемые сценарии:

Проверка форматов идентификаторов:

Joi.string().custom((value, helpers) => {
  if (!value.startsWith('ID-')) {
    return helpers.error('string.invalidFormat');
  }
  return value;
});

Проверка диапазонов с бизнес-логикой:

Joi.number().custom((value, helpers) => {
  if (value % 10 !== 0) {
    return helpers.error('number.notRounded');
  }
  return value;
});

Условная логика на основе внешних параметров:

const isProduction = true;

Joi.string().custom((value, helpers) => {
  if (isProduction && value === 'debug') {
    return helpers.error('string.forbidden');
  }
  return value;
});

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