Breaking changes

Общая природа breaking changes в Joi

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

Основная причина появления breaking changes заключалась в стремлении:

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

Переход от @hapi/joi к joi: изменение пространства имён

Одним из значимых переломных моментов стало разделение библиотеки на отдельный пакет joi. Ранее использовался scoped-пакет @hapi/joi, тесно связанный с экосистемой Hapi.

Изменения:

  • изменение имени пакета и точки импорта
  • переработка сборки и зависимостей
  • частичное упрощение внутренней архитектуры
// старый вариант
const Joi = require('@hapi/joi');

// новый вариант
const Joi = require('joi');

Переход не ограничивался косметическим изменением импорта — внутренняя логика обработки схем была переработана, что повлияло на поведение ряда методов.


Изменения в поведении валидации

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

1. Строгость по умолчанию

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

Joi.number().validate('5'); // ранее: 5
// теперь: ошибка или требуется включение convert

Поведение стало зависеть от опции convert, которая контролирует приведение типов.


2. Изменение presence и обязательности

Ранее использование required() имело менее строгую модель проверки. В новых версиях:

  • поведение required, optional, forbidden унифицировано
  • изменена логика наследования обязательности в object
const schema = Joi.object({
  a: Joi.string()
}).required();

В новых версиях обязательность объекта не всегда автоматически распространяется на вложенные поля.


3. Изменения в альтернативных схемах

Механизм alternatives() был переработан:

  • изменён порядок выбора подходящей схемы
  • улучшена детерминированность результата
  • убрана часть неявных fallback-веток
const schema = Joi.alternatives().try(
  Joi.string(),
  Joi.number()
);

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


Изменения в API методов

1. validate() и validateAsync()

Существенное изменение связано с разделением синхронной и асинхронной валидации.

  • validate() остаётся синхронным
  • validateAsync() стал основным способом работы с асинхронными правилами
const result = await schema.validateAsync(data);

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


2. Удаление или изменение устаревших методов

Некоторые методы были удалены или заменены:

  • Joi.validate() как статическая функция
  • часть алиасов для типов (Joi.int и подобные)
  • устаревшие цепочки конфигурации

3. Изменение chainable API

Цепочки методов стали более строгими:

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

Пример:

Joi.string().min(3).max(10).required();

Теперь порядок и совместимость модификаторов проверяются более жёстко.


Изменения в системе ошибок

1. Структура ValidationError

Формат ошибок был переработан:

  • унифицировано поле details
  • изменена структура context
  • улучшена типизация причины ошибки
{
  "message": "...",
  "details": [
    {
      "message": "...",
      "path": ["field"],
      "type": "string.min"
    }
  ]
}

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


2. Кодирование ошибок (error codes)

Введена более строгая система кодов:

  • стандартизированы типы ошибок (any.required, string.base)
  • убраны дублирующиеся обозначения
  • добавлена консистентность между типами

3. Изменение работы abortEarly

Поведение опции стало более предсказуемым:

  • при abortEarly: true возвращается первая ошибка
  • при false — полный список без частичных пропусков

Ранее возможны были случаи неполного сбора ошибок в сложных схемах.


Изменения в работе с объектами и вложенными схемами

1. Merge схем через concat

Поведение concat() было уточнено:

  • конфликтующие правила теперь имеют детерминированный приоритет
  • устранены неоднозначности при слиянии объектов
const base = Joi.object({ a: Joi.string() });
const extended = base.concat(Joi.object({ b: Joi.number() }));

2. Вложенные object схемы

Изменена обработка вложенных структур:

  • улучшена проверка глубоко вложенных объектов
  • изменена логика наследования unknown()

Изменения в ref-ссылках

Механизм Joi.ref() получил обновлённую семантику:

  • изменена интерпретация путей
  • улучшена работа с вложенными объектами
  • добавлена более строгая проверка циклических ссылок
Joi.object({
  a: Joi.number(),
  b: Joi.ref('a')
});

Ранее возможны были случаи неоднозначного разрешения пути.


Изменения в string и number валидаторах

String
  • переработана работа trim()
  • изменено поведение pattern() при флагах регулярных выражений
  • более строгая обработка Unicode
Number
  • изменено поведение precision()
  • усилена проверка NaN и Infinity
  • убраны некоторые неявные преобразования строк в числа

Изменения в allowUnknown и stripUnknown

allowUnknown

Поведение стало более предсказуемым:

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

Совместимость и миграция схем

Миграция между версиями требует пересмотра следующих аспектов:

  • использование validate() vs validateAsync()
  • пересмотр всех неявных преобразований типов
  • проверка схем с alternatives()
  • анализ поведения required() на вложенных объектах
  • обновление обработки ошибок

Особое внимание требуется схемам, которые опирались на:

  • нестрогую типизацию входных данных
  • неявные fallback-ветки
  • устаревшие алиасы методов

Изменения в TypeScript-совместимости

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

  • улучшена генерация типов схем
  • расширена поддержка inference
  • уменьшено количество any в типах API

Однако часть старых деклараций стала несовместимой:

  • изменены типы возвращаемых значений validate
  • уточнены generics для object() и array() схем