Частые ошибки и решения

Несоответствие между required() и фактической логикой приложения

Одна из самых распространённых проблем — несогласованность схемы валидации с бизнес-логикой. Поле объявляется обязательным через required(), хотя на практике может отсутствовать.

const Joi = require('joi');

const schema = Joi.object({
  username: Joi.string().required(),
  avatar: Joi.string().required()
});

Если avatar действительно необязателен, схема начинает выбрасывать ошибки в корректных сценариях.

Правильный вариант:

const schema = Joi.object({
  username: Joi.string().required(),
  avatar: Joi.string().optional()
});

Либо:

avatar: Joi.string().allow(null)

Ошибка: undefined и null — разные значения

Многие ожидают, что optional() автоматически разрешает null.

Это неверно.

const schema = Joi.object({
  age: Joi.number().optional()
});

Допустимо:

{}

Недопустимо:

{
  age: null
}

Для поддержки null требуется:

const schema = Joi.object({
  age: Joi.number().allow(null)
});

Комбинация:

age: Joi.number().optional().allow(null)

означает:

  • поле можно не передавать;
  • поле может быть null;
  • поле может быть числом.

Ошибки преобразования типов

Неожиданное приведение типов

Joi автоматически конвертирует значения.

Пример:

const schema = Joi.object({
  age: Joi.number()
});

schema.validate({
  age: '25'
});

Строка '25' превратится в число 25.

Иногда это полезно, но может приводить к скрытым ошибкам.

Отключение автоматического преобразования

schema.validate(data, {
  convert: false
});

Теперь:

{
  age: '25'
}

вызовет ошибку:

"age" must be a number

Ошибка при работе с boolean

const schema = Joi.object({
  isAdmin: Joi.boolean()
});

Joi автоматически преобразует:

'true'  -> true
'false' -> false

При строгой типизации лучше отключать convert.


Ошибки при валидации строк

Пустая строка считается валидной

Joi.string()

разрешает:

''

Это часто становится причиной багов.

Запрет пустых строк

Joi.string().min(1)

или:

Joi.string().empty('')

Пример:

const schema = Joi.object({
  username: Joi.string().min(3).required()
});

Ошибки использования allow()

allow('') ломает ограничения

Пример:

Joi.string().min(5).allow('')

Пустая строка теперь полностью проходит валидацию, несмотря на min(5).

Это связано с тем, что allow() добавляет значение в список допустимых исключений.


Ошибки при работе с объектами

Неизвестные поля запрещены

По умолчанию Joi отклоняет лишние поля.

const schema = Joi.object({
  username: Joi.string()
});

Ошибка:

{
  username: 'alex',
  role: 'admin'
}

Сообщение:

"role" is not allowed

Разрешение дополнительных полей

Joi.object({
  username: Joi.string()
}).unknown(true)

Ошибка чрезмерного использования unknown(true)

Разрешение любых полей может создавать уязвимости.

Опасный пример:

{
  username: 'alex',
  isAdmin: true
}

Если объект напрямую сохраняется в БД, злоумышленник может передать неожиданные свойства.

Безопаснее явно описывать структуру.


Ошибки вложенной валидации

Отсутствие схемы для внутренних объектов

Плохой пример:

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

Такой объект пропускает практически любые данные.

Правильный вариант:

const schema = Joi.object({
  profile: Joi.object({
    firstName: Joi.string().required(),
    lastName: Joi.string().required()
  })
});

Ошибки при работе с массивами

Отсутствие описания элементов массива

Joi.array()

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

Правильная схема

Joi.array().items(Joi.string())

Пример:

const schema = Joi.object({
  tags: Joi.array().items(Joi.string())
});

Ошибка: пустой массив считается валидным

Joi.array()

разрешает:

[]

Запрет пустых массивов

Joi.array().min(1)

Ошибки при использовании valid()

valid() делает список строго ограниченным

Joi.string().valid('admin', 'user')

Теперь любые другие значения запрещены.

Ошибка возникает, когда разработчик ожидает лишь рекомендацию, а получает жёсткое ограничение.


Ошибки при работе с датами

Joi принимает строки как даты

Joi.date()

валидирует:

'2025-01-01'

Проблемы часовых поясов

new Date('2025-01-01')

может интерпретироваться по-разному в зависимости от окружения.

Более безопасный вариант

Joi.date().iso()

Ошибки в кастомных валидаторах

Отсутствие возврата значения

Неверно:

const schema = Joi.string().custom((value, helpers) => {
  if (value.includes('admin')) {
    helpers.error('string.invalid');
  }
});

Ошибка: валидатор не возвращает значение.

Правильно:

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

  return value;
});

Ошибки при использовании messages()

Неверные ключи сообщений

Joi.string().messages({
  required: 'Поле обязательно'
});

Так работать не будет.

Нужны полные коды ошибок:

Joi.string().messages({
  'any.required': 'Поле обязательно'
});

Ошибки обработки error.details

Использование только первого сообщения

const { error } = schema.validate(data);

if (error) {
  console.log(error.details[0].message);
}

Проблема: остальные ошибки теряются.

Сбор всех ошибок

const messages = error.details.map(item => item.message);

Ошибка преждевременного завершения валидации

По умолчанию Joi останавливается после первой ошибки.

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

schema.validate(data);

Получение всех ошибок

schema.validate(data, {
  abortEarly: false
});

Ошибки условной валидации

Неправильное использование when()

Ошибка:

Joi.string().when('role', {
  is: 'admin',
  then: Joi.required()
});

Проблема возникает, если role отсутствует или имеет другой тип.

Полная схема

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

  accessCode: Joi.string().when('role', {
    is: 'admin',
    then: Joi.required(),
    otherwise: Joi.optional()
  })
});

Ошибки с default()

Значение по умолчанию не меняет исходный объект

const schema = Joi.object({
  role: Joi.string().default('user')
});

После валидации:

const result = schema.validate({});

Значение будет находиться здесь:

result.value

а не в исходном объекте.


Ошибки при повторном использовании схем

Изменение исходной схемы

const base = Joi.string();

const requiredField = base.required();

Joi создаёт новую схему, а не мутирует старую.

Иногда разработчики ожидают обратное.


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

Слишком доверительная проверка

Joi.string().email()

проверяет только формат.

Это не означает:

  • существование почты;
  • наличие домена;
  • возможность отправки письма.

Ошибки безопасности

Валидация только на клиенте

Даже если Joi используется во frontend-приложении:

const schema = Joi.object({
  username: Joi.string().required()
});

сервер обязан выполнять повторную проверку.

Клиентская валидация никогда не считается безопасной.


Ошибки при работе с API

Разные схемы для create и update

Частая проблема — использование одной схемы:

const userSchema = Joi.object({
  username: Joi.string().required(),
  email: Joi.string().email().required()
});

Для PATCH-запросов такая схема неудобна.

Решение через fork()

const updateSchema = userSchema.fork(
  ['username', 'email'],
  field => field.optional()
);

Ошибки производительности

Создание схем внутри обработчиков

Плохой пример:

app.post('/users', (req, res) => {
  const schema = Joi.object({
    username: Joi.string().required()
  });

  schema.validate(req.body);
});

Схема создаётся при каждом запросе.

Правильный подход

const schema = Joi.object({
  username: Joi.string().required()
});

app.post('/users', (req, res) => {
  schema.validate(req.body);
});

Ошибки при асинхронной валидации

Использование validate() вместо validateAsync()

Если присутствуют асинхронные правила:

await schema.validateAsync(data);

иначе возможны непредсказуемые ошибки.


Ошибки при работе с strip()

Потеря данных

password: Joi.string().strip()

Поле будет удалено из результата.

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


Ошибки миграции между версиями Joi

Старый синтаксис

Некоторые старые примеры используют:

Joi.validate(data, schema);

В современных версиях:

schema.validate(data);

Ошибки интеграции с Express

Игнорирование результата валидации

Ошибка:

schema.validate(req.body);

next();

Валидация выполняется, но результат не проверяется.

Правильно:

const { error, value } = schema.validate(req.body);

if (error) {
  return res.status(400).json({
    error: error.details
  });
}

req.body = value;

next();

Ошибки при работе с числами

number() принимает Infinity

Неочевидное поведение:

Joi.number()

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

Ограничение диапазона

Joi.number().min(0).max(100)

Ошибки при использовании regex()

Нечитаемые регулярные выражения

Плохой пример:

Joi.string().pattern(/^(?=.*[A-Z])(?=.*\d).+$/)

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

Более читаемый вариант

const PASSWORD_REGEX =
  /^(?=.*[A-Z])(?=.*\d).+$/;

const schema = Joi.string().pattern(PASSWORD_REGEX);

Ошибки валидации паролей

Проверка только длины

Joi.string().min(8)

не обеспечивает безопасность.

Минимально полезная схема:

Joi.string()
  .min(8)
  .pattern(/[A-Z]/)
  .pattern(/[a-z]/)
  .pattern(/[0-9]/)

Ошибки при использовании alternatives()

Конфликтующие схемы

Joi.alternatives().try(
  Joi.string(),
  Joi.number()
)

Иногда значения неожиданно проходят из-за автоматической конвертации.

Пример:

'123'

может быть воспринято как число.

Решение

schema.validate(data, {
  convert: false
});

Ошибки проектирования схем

Огромные монолитные схемы

Плохой подход:

const schema = Joi.object({
  // сотни полей
});

Поддержка таких схем быстро усложняется.

Разделение схем

const addressSchema = Joi.object({
  city: Joi.string(),
  street: Joi.string()
});

const userSchema = Joi.object({
  username: Joi.string(),
  address: addressSchema
});

Ошибки тестирования

Отсутствие тестов схем

Даже простая схема может содержать ошибки:

Joi.string().email()

Без тестов сложно гарантировать корректность поведения.

Пример теста:

expect(schema.validate({
  email: 'wrong'
}).error).toBeDefined();

Практические рекомендации

Использование строгих схем

Joi.object({
  username: Joi.string().required()
})
  .required()
  .unknown(false);

Централизация схем

schemas/
  user.schema.js
  auth.schema.js
  product.schema.js

Повторное использование правил

const emailRule = Joi.string().email().required();

Явная настройка поведения

schema.validate(data, {
  abortEarly: false,
  convert: false
});

Разделение create/update схем

createUserSchema
updateUserSchema

Обязательная серверная валидация

Joi должен использоваться:

  • в API;
  • в микросервисах;
  • в очередях сообщений;
  • при обработке внешних данных;
  • при загрузке файлов;
  • при интеграциях со сторонними сервисами.