Макросы и шаблоны

В контексте Ajv макросы представляют собой способ абстрагирования и переиспользования логики JSON Schema за счёт генерации новых схем на этапе компиляции. В отличие от обычной валидации, где схема фиксирована, макросы позволяют описывать шаблон поведения, который преобразуется в стандартную JSON Schema перед выполнением проверки.

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


Кастомные ключевые слова как основа макросов

В Ajv расширение функциональности реализуется через пользовательские ключевые слова (custom keywords). Они могут работать в трёх режимах:

  • validate — выполняет ручную проверку
  • compile — генерирует функцию-валидатор
  • macro — преобразует схему в другую схему

Именно режим macro является основой шаблонного подхода.

Пример базовой структуры:

ajv.addKeyword({
  keyword: "range",
  macro: (schema) => {
    const { min, max } = schema;

    return {
      type: "number",
      minimum: min,
      maximum: max
    };
  }
});

Здесь ключевое слово range не проверяет значение напрямую. Оно разворачивается в стандартные ограничения minimum и maximum.


Принцип работы macro-подхода

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

  1. Исходная схема анализируется Ajv
  2. Встречается кастомное ключевое слово с macro
  3. Функция возвращает новую JSON Schema
  4. Результат компилируется как обычная схема

Таким образом достигается:

  • уменьшение дублирования
  • создание DSL поверх JSON Schema
  • композиция сложных правил

Макросы как шаблоны бизнес-логики

Макросы часто используются для выражения повторяющихся ограничений.

Пример: условная длина строки

ajv.addKeyword({
  keyword: "minLengthIf",
  macro: (schema) => {
    const { condition, minLength } = schema;

    return {
      type: "string",
      minLength: {
        if: condition,
        then: minLength,
        else: 0
      }
    };
  }
});

Хотя JSON Schema напрямую не поддерживает такую конструкцию в данном виде, макрос позволяет инкапсулировать логику на уровне декларации.


Шаблонные схемы и параметризация

Макросы фактически превращаются в параметризованные шаблоны схем.

Пример шаблона диапазона:

ajv.addKeyword({
  keyword: "between",
  macro: (schema) => {
    return {
      type: "number",
      minimum: schema[0],
      maximum: schema[1]
    };
  }
});

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

{
  between: [10, 20]
}

Результат трансформации:

{
  type: "number",
  minimum: 10,
  maximum: 20
}

Такой подход позволяет формировать компактные декларации вместо громоздких JSON Schema-объектов.


Макросы и переиспользование через $defs

Помимо макросов, важную роль в шаблонизации играет механизм $defs в Ajv.

Он позволяет определять повторно используемые фрагменты схем:

{
  $defs: {
    positiveNumber: {
      type: "number",
      minimum: 0
    }
  },
  type: "object",
  properties: {
    price: { $ref: "#/$defs/positiveNumber" }
  }
}

Макросы и $defs часто используются вместе:

  • $defs — для статических повторяющихся блоков
  • macro — для динамически генерируемых схем

Динамические шаблоны через $data

Ещё один уровень шаблонизации достигается через $data-ссылки.

{
  type: "object",
  properties: {
    min: { type: "number" },
    max: { type: "number" },
    value: {
      type: "number",
      minimum: { $data: "1/min" },
      maximum: { $data: "1/max" }
    }
  }
}

Здесь схема зависит от значений внутри объекта.

Макросы и $data решают разные задачи:

  • макросы формируют структуру схемы
  • $data делает её зависимой от входных данных

Композиция схем как шаблонный механизм

Вместо макросов иногда используется композиция:

  • allOf
  • anyOf
  • oneOf

Пример шаблонного объединения:

{
  allOf: [
    { type: "string" },
    { minLength: 5 },
    { pattern: "^[a-zA-Z]+$" }
  ]
}

Макрос в этом случае может упростить выражение:

ajv.addKeyword({
  keyword: "alphaString",
  macro: (schema) => ({
    type: "string",
    minLength: schema.minLength || 1,
    pattern: "^[a-zA-Z]+$"
  })
});

Реализация сложных шаблонов

Макросы позволяют строить DSL для предметной области.

Пример: email с политиками

ajv.addKeyword({
  keyword: "businessEmail",
  macro: (schema) => {
    const domain = schema.domain;

    return {
      type: "string",
      format: "email",
      pattern: `@${domain}$`
    };
  }
});

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

{
  businessEmail: { domain: "company.com" }
}

Ограничения макросов

Несмотря на гибкость, макросы имеют ограничения:

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

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


Практика организации библиотеки макросов

В крупных приложениях макросы обычно выделяются в отдельный модуль:

/schemas
  /keywords
    range.js
    between.js
    businessEmail.js
  /defs
    primitives.js

И регистрируются централизованно:

import { addRange } from "./keywords/range";
import { addBetween } from "./keywords/between";

export function registerKeywords(ajv) {
  addRange(ajv);
  addBetween(ajv);
}

Такой подход позволяет:

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

Макросы как уровень абстракции над JSON Schema

В итоге макросы формируют дополнительный слой над Ajv, позволяя:

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

При этом базовая JSON Schema остаётся конечной точкой исполнения, а макросы лишь трансформируют входное описание в её стандартное представление.