Extend API

Extend API в Joi представляет собой механизм расширения стандартного набора валидаторов и типов за счёт создания собственных правил, типов схем и поведения валидации. Этот инструмент используется тогда, когда встроенных возможностей библиотеки недостаточно для выражения предметной области или требуется переиспользуемая доменная логика валидации.

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

Базовая идея:

  • создаётся новый тип или модифицируется существующий;
  • добавляются правила (rules);
  • при необходимости переопределяется поведение кастомной валидации;
  • описываются сообщения об ошибках.

Базовая структура Joi.extend

Общая форма расширения выглядит следующим образом:

const ExtendedJoi = Joi.extend((joi) => ({
    type: 'customType',
    base: joi.string(),
    messages: {
        'customType.invalid': '{{#label}} имеет недопустимое значение'
    },
    rules: {
        myRule: {
            validate(value, helpers, args, options) {
                return value;
            }
        }
    }
}));

Ключевые элементы:

  • type — имя нового типа схемы;
  • base — базовый тип Joi, от которого наследуется поведение;
  • messages — набор кастомных сообщений об ошибках;
  • rules — расширяемые правила валидации;
  • validate — функция, реализующая логику проверки.

Создание пользовательского типа

Создание нового типа позволяет описать семантически отдельную сущность, например email с дополнительными ограничениями, идентификатор формата или бизнес-объект.

Пример расширения для пользовательского типа «slug»:

const JoiSlug = Joi.extend((joi) => ({
    type: 'slug',
    base: joi.string(),
    messages: {
        'slug.invalid': '{{#label}} должен содержать только латиницу, цифры и дефисы'
    },
    validate(value, helpers) {
        const isValid = /^[a-z0-9-]+$/.test(value);

        if (!isValid) {
            return { value, errors: helpers.error('slug.invalid') };
        }

        return { value };
    }
}));

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

const schema = JoiSlug.slug().required();

schema.validate('my-valid-slug'); // ok
schema.validate('invalid slug!'); // error

Добавление правил (rules)

Rules позволяют добавлять цепочечные методы к существующему типу. Это ключевая возможность Extend API, обеспечивающая гибкость DSL-подобного синтаксиса.

Пример добавления правила минимальной длины без использования встроенного min:

const Extended = Joi.extend((joi) => ({
    type: 'string',
    base: joi.string(),
    rules: {
        strictMin: {
            method(length) {
                return this.$_addRule({ name: 'strictMin', args: { length } });
            },
            args: [
                {
                    name: 'length',
                    assert: (value) => typeof value === 'number',
                    message: 'length must be a number'
                }
            ],
            validate(value, helpers, args) {
                if (value.length < args.length) {
                    return helpers.error('string.min', { limit: args.length });
                }

                return value;
            }
        }
    }
}));

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

const schema = Extended.string().strictMin(5);

schema.validate('abc'); // error
schema.validate('abcdef'); // ok

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

Функция validate внутри rules и type получает несколько полезных параметров:

  • value — текущее значение;
  • helpers — набор утилит для генерации ошибок и преобразований;
  • args — аргументы правила;
  • state — состояние валидации (цепочка вызовов);
  • options — глобальные опции валидации.

Пример использования helpers:

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

    return value;
}

Кастомные сообщения ошибок

Сообщения задаются через messages, используя ключи, связанные с правилами или типами:

messages: {
    'string.strictMin': '{{#label}} должен быть не менее {{#limit}} символов'
}

Подстановки:

  • {{#label}} — имя поля;
  • {{#value}} — текущее значение;
  • {{#limit}} — пользовательский параметр.

Расширение существующих типов

Extend API позволяет модифицировать поведение встроенных типов, например string, number, object.

Пример добавления правила к number:

const JoiExtended = Joi.extend((joi) => ({
    type: 'number',
    base: joi.number(),
    rules: {
        even: {
            validate(value, helpers) {
                if (value % 2 !== 0) {
                    return helpers.error('number.even');
                }

                return value;
            }
        }
    },
    messages: {
        'number.even': '{{#label}} должно быть чётным числом'
    }
}));

Взаимодействие с объектными схемами

Пользовательские типы можно свободно использовать внутри object() схем:

const schema = Joi.object({
    id: JoiExtended.slug().required(),
    age: JoiExtended.number().even()
});

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


Композиция расширений

Joi позволяет комбинировать несколько расширений последовательно:

const A = Joi.extend(extensionA);
const B = A.extend(extensionB);

Каждый новый слой сохраняет предыдущие расширения, формируя цепочку типов.


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

При сложной валидации полезен доступ к контексту:

  • state.path — путь к текущему полю;
  • state.ancestors — родительские значения;
  • state.reference — ссылки на другие части схемы.

Пример проверки зависимостей:

validate(value, helpers, args, options) {
    const parent = helpers.state.ancestors[0];

    if (parent.role === 'admin' && value !== 'allowed') {
        return helpers.error('any.invalid');
    }

    return value;
}

Типизация и TypeScript-расширения

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

declare module 'joi' {
    interface StringSchema {
        slug(): this;
    }
}

Это обеспечивает корректную работу цепочек методов:

Joi.slug().required();

Переиспользуемые паттерны расширений

На практике Extend API используется для:

  • доменных типов (email с бизнес-ограничениями, ID форматов);
  • нормализации входных данных;
  • централизованной бизнес-валидации;
  • переиспользуемых правил между сервисами;
  • унификации API-валидации.

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

Типичные проблемы при использовании Extend API:

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

Extend API формирует уровень абстракции над базовыми примитивами Joi, позволяя переносить бизнес-логику в слой схем и создавать выразительные доменно-ориентированные валидаторы, интегрированные в стандартный механизм проверки данных.