Messages для кастомных типов

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


Архитектура сообщений в расширениях

Кастомный тип в Joi создаётся через Joi.extend. Внутри расширения сообщения определяются на уровне типа и отдельных правил. Базовая структура включает:

  • type — имя нового типа
  • base — базовый валидатор
  • messages — набор сообщений по умолчанию
  • rules — пользовательские правила валидации
const Joi = require('joi');

const JoiPositiveInt = Joi.extend((joi) => ({
  type: 'positiveInt',
  base: joi.number(),
  messages: {
    'positiveInt.base': '{{#label}} должен быть положительным целым числом',
    'positiveInt.max': '{{#label}} не может быть больше {{#limit}}',
  },
  rules: {
    positive: {
      validate(value, helpers) {
        if (!Number.isInteger(value) || value <= 0) {
          return helpers.error('positiveInt.base');
        }
        return value;
      }
    },
    max: {
      method(limit) {
        return this.$_addRule({ name: 'max', args: { limit } });
      },
      args: [
        {
          name: 'limit',
          assert: (v) => typeof v === 'number',
          message: 'должен быть числом'
        }
      ],
      validate(value, helpers, args) {
        if (value > args.limit) {
          return helpers.error('positiveInt.max', { limit: args.limit });
        }
        return value;
      }
    }
  }
}));

Ключевая идея контекстных сообщений

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

  • {{#label}} — имя поля
  • {{#value}} — текущее значение
  • {{#limit}} — ограничение из аргументов правила
  • {{#key}} — внутренний ключ ошибки

Пример:

messages: {
  'positiveInt.max': '{{#label}} = {{#value}} превышает допустимый предел {{#limit}}'
}

При срабатывании ошибки сообщение автоматически подставляет значения из контекста, формируемого в helpers.error.


Генерация ошибок внутри кастомного правила

Внутри validate используется helpers.error, который связывает правило с ключом сообщения:

validate(value, helpers) {
  if (value < 0) {
    return helpers.error('positiveInt.base');
  }
  return value;
}

Связь между строкой ошибки и сообщением устанавливается через ключ positiveInt.base, определённый в messages.


Переопределение сообщений на уровне схемы

Сообщения кастомного типа можно переопределять локально:

const schema = JoiPositiveInt.positive().max(10).messages({
  'positiveInt.max': 'Слишком большое значение: максимум {{#limit}}'
});

Приоритет сообщений:

  1. Локальные .messages() у схемы
  2. Сообщения внутри расширения (Joi.extend)
  3. Встроенные сообщения Joi

Работа с несколькими правилами внутри одного типа

Кастомный тип может содержать множество правил, каждое из которых имеет собственный набор сообщений:

messages: {
  'positiveInt.base': 'Ошибка базового типа',
  'positiveInt.positive': 'Значение должно быть > 0',
  'positiveInt.max': 'Превышен максимум {{#limit}}',
  'positiveInt.min': 'Меньше минимального значения {{#limit}}'
}

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


Контекст ошибок и передача параметров

При вызове helpers.error можно передавать дополнительные данные:

return helpers.error('positiveInt.max', {
  limit: args.limit,
  value
});

Эти параметры становятся доступны в шаблоне сообщения:

  • {{#limit}}
  • {{#value}}

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


Наследование сообщений от базового Joi

При расширении базового типа (например, joi.number()), остаются доступны стандартные ключи сообщений:

  • number.base
  • number.min
  • number.max

Их можно переопределять внутри кастомного типа:

messages: {
  'number.base': 'Ожидалось число, получено другое значение'
}

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


Ошибки отсутствия ключей сообщений

Если helpers.error вызывает ключ, который не определён в messages, результат будет зависеть от конфигурации Joi:

  • либо стандартное сообщение
  • либо пустой текст ошибки
  • либо fallback от родительского типа

Это делает обязательным строгий контроль соответствия ключей и сообщений.


Организация крупных наборов сообщений

В сложных схемах удобно разделять сообщения по слоям:

messages: {
  // базовый уровень
  'positiveInt.base': '...',

  // правила
  'positiveInt.min': '...',
  'positiveInt.max': '...',

  // системные переопределения
  'number.base': '...'
}

Такой подход упрощает поддержку кастомных типов при расширении библиотеки.


Использование функций вместо строк

Joi поддерживает динамическую генерацию сообщений через функции:

messages: {
  'positiveInt.max': (context) =>
    `${context.label} превышает максимум ${context.limit}`
}

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


Поведение при композиции типов

При комбинировании кастомных типов с .concat() или .alter() сообщения могут конфликтовать. Приоритет сохраняется по правилу:

  • последний применённый тип переопределяет сообщения предыдущего

Это важно учитывать при построении сложных схем расширений.


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

При разработке кастомных типов в Joi логически разделяются три слоя сообщений:

  • сообщения базовой валидации
  • сообщения пользовательских правил
  • сообщения уровня схемы

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