Динамические значения через функции

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

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


Динамические значения по умолчанию через .default()

Метод .default() поддерживает передачу функции, что делает возможным генерацию значения на основе внешних факторов или входных данных.

const schema = Joi.object({
  createdAt: Joi.date().default(() => new Date())
});

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

// ❌ фиксированное значение вычисляется один раз при объявлении схемы
Joi.date().default(new Date())

Динамическая форма особенно полезна при работе с временными метками, идентификаторами и производными значениями.


Контекстные вычисления в .default((value, helpers) => {})

Функция может принимать параметры контекста, позволяющие учитывать входные данные.

const schema = Joi.object({
  fullName: Joi.string(),
  slug: Joi.string().default((value, helpers) => {
    const { fullName } = helpers.state.ancestors[0];
    return fullName.toLowerCase().replace(/\s+/g, '-');
  })
});

Здесь значение slug вычисляется на основе другого поля объекта. Такой подход используется для производных данных, где одно поле зависит от другого.


Использование .custom() для произвольной логики

Метод .custom() позволяет внедрять функцию в процесс валидации. Это один из наиболее гибких инструментов Joi.

const schema = Joi.number().custom((value, helpers) => {
  if (value % 2 !== 0) {
    return helpers.error('number.even');
  }
  return value;
});

Функция получает текущее значение и объект helpers, через который можно:

  • возвращать ошибку через helpers.error
  • модифицировать результат
  • использовать контекст валидации

Дополнительно возможно использование внешнего состояния:

const schema = Joi.string().custom((value, helpers) => {
  const banned = helpers.prefs.context?.bannedWords || [];
  if (banned.includes(value)) {
    return helpers.error('string.banned');
  }
  return value;
});

Динамические схемы через .lazy()

Когда структура данных зависит от входного значения, используется Joi.lazy().

const schema = Joi.lazy(value => {
  if (typeof value === 'number') {
    return Joi.number().min(0);
  }
  return Joi.string().max(100);
});

Функция вызывается в момент валидации и возвращает схему, соответствующую текущему значению.

Типичные сценарии:

  • разные типы данных в одном поле API
  • условная структура JSON
  • адаптивные схемы конфигураций

Условная логика с функциями и .when()

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

const schema = Joi.object({
  role: Joi.string(),
  accessLevel: Joi.number().when('role', {
    is: (role) => role === 'admin',
    then: Joi.number().min(10),
    otherwise: Joi.number().max(5)
  })
});

Функция в is позволяет строить сложные условия, выходящие за рамки простого сравнения.


Динамические сообщения об ошибках

Функции применяются и для формирования сообщений, зависящих от контекста.

const schema = Joi.number().min(10).messages({
  'number.min': (context) => `Значение ${context.value} меньше допустимого`
});

Контекст ошибки содержит:

  • текущее значение
  • тип ошибки
  • путь в объекте

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


Контекст выполнения через prefs.context

Одним из ключевых механизмов передачи динамических данных является контекст валидации.

const schema = Joi.string().custom((value, helpers) => {
  const locale = helpers.prefs.context?.locale;

  if (locale === 'ru' && value.length < 3) {
    return helpers.error('string.tooShort');
  }

  return value;
});

Контекст задаётся при вызове:

schema.validate(data, {
  context: { locale: 'ru' }
});

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


Ленивые значения и зависимость от рантайма

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

const schema = Joi.object({
  config: Joi.lazy(() => {
    const env = process.env.NODE_ENV;

    if (env === 'production') {
      return Joi.object({
        cache: Joi.boolean().valid(true)
      });
    }

    return Joi.object({
      cache: Joi.boolean()
    });
  })
});

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


Ограничения и особенности выполнения функций

Функции в Joi выполняются в строго определённых точках:

  • при вычислении значений по умолчанию
  • во время валидации (custom)
  • при построении схемы (lazy)
  • при формировании сообщений

Важно учитывать, что:

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

Композиция функций в сложных схемах

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

const schema = Joi.object({
  type: Joi.string(),

  data: Joi.lazy((value, helpers) => {
    const parent = helpers.state.ancestors[0];

    if (parent.type === 'A') {
      return Joi.object({
        value: Joi.number().default(() => Math.random() * 100)
      });
    }

    return Joi.object({
      value: Joi.string().default(() => 'default')
    });
  })
});

Здесь одновременно используются:

  • lazy для выбора структуры
  • default с функцией для генерации значений
  • контекст родительского объекта

Функции как основа расширяемой валидации

Функциональный подход в Joi превращает схему из статического описания в вычисляемую систему правил. Это позволяет строить:

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

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