Создание собственных ключевых слов

Валидация данных в Ajv строится на основе JSON Schema, однако стандартного набора ключевых слов часто оказывается недостаточно для сложных прикладных задач. Расширяемость библиотеки реализована через механизм пользовательских ключевых слов (custom keywords), позволяющий внедрять собственные правила валидации, трансформации данных и генерации кода схемы.

Модель расширения через ключевые слова

Каждое ключевое слово в Ajv — это часть компиляционного процесса схемы. При обработке JSON Schema библиотека преобразует её в функцию валидации. Пользовательские ключевые слова подключаются на этапе компиляции и могут влиять на:

  • логику проверки значения;
  • изменение данных (side effects);
  • генерацию оптимизированного кода;
  • условия применения других правил схемы.

Механизм расширения работает через регистрацию ключевого слова в экземпляре Ajv:

import Ajv from "ajv";

const ajv = new Ajv();

Базовая регистрация ключевого слова

Минимальная форма определения нового ключевого слова включает функцию валидации:

ajv.addKeyword({
  keyword: "isPositive",
  validate: function (schema, data) {
    return typeof data === "number" && data > 0;
  }
});

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

const schema = {
  type: "number",
  isPositive: true
};

const validate = ajv.compile(schema);

validate(10); // true
validate(-5); // false

В этом примере значение schema передаётся в функцию как schema параметр, но чаще всего оно используется как флаг или конфигурация поведения.

Типы пользовательских ключевых слов

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

1. Валидирующие ключевые слова

Это наиболее распространённый тип. Они возвращают true или false и участвуют в процессе валидации:

ajv.addKeyword({
  keyword: "minWords",
  type: "string",
  validate: function (schema, data) {
    if (typeof data !== "string") return false;
    return data.trim().split(/\s+/).length >= schema;
  }
});

Схема:

const schema = {
  type: "string",
  minWords: 3
};

2. Ключевые слова с компиляцией (code generation)

Для повышения производительности Ajv позволяет генерировать JavaScript-код вместо интерпретируемых функций.

ajv.addKeyword({
  keyword: "multipleOf2",
  type: "number",
  compile: function (schema) {
    return function (data) {
      return data % 2 === 0;
    };
  }
});

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

3. Декларативные ключевые слова (meta-schema based)

Такие ключевые слова не содержат логики, а расширяют схему через под-схемы:

ajv.addKeyword({
  keyword: "evenNumber",
  metaSchema: {
    type: "boolean"
  },
  validate: function (schema, data) {
    return !schema || (typeof data === "number" && data % 2 === 0);
  }
});

Использование компилятора для доступа к runtime данным

Ajv позволяет создавать более сложные ключевые слова через контекст валидации:

ajv.addKeyword({
  keyword: "greaterThanField",
  type: "number",
  errors: true,
  compile: function (schema, parentSchema) {
    return function (data, dataPath, parentData, parentDataProperty) {
      const compareValue = parentData[schema];
      return data > compareValue;
    };
  }
});

Схема:

const schema = {
  type: "object",
  properties: {
    min: { type: "number" },
    max: { type: "number", greaterThanField: "min" }
  }
};

Здесь используется доступ к соседним полям объекта, что невозможно выразить стандартными средствами JSON Schema.

Управление ошибками в пользовательских ключевых словах

Для детализированных сообщений об ошибках используется errors: true и ручное формирование массива ошибок:

ajv.addKeyword({
  keyword: "positive",
  type: "number",
  errors: true,
  validate: function (schema, data) {
    const valid = typeof data === "number" && data > 0;

    if (!valid) {
      validate.errors = [
        {
          keyword: "positive",
          message: "значение должно быть положительным числом",
          params: { value: data }
        }
      ];
    }

    return valid;
  }
});

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

Асинхронные пользовательские ключевые слова

Ajv поддерживает асинхронную валидацию, включая async/await:

ajv.addKeyword({
  keyword: "existsInDatabase",
  async: true,
  type: "string",
  validate: async function (schema, data) {
    const result = await fakeDbLookup(data);
    return result.exists;
  }
});

Схема:

const schema = {
  type: "string",
  existsInDatabase: true
};

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

const validate = ajv.compile(schema);

await validate("test-value");

Параметризованные ключевые слова

Ключевые слова могут принимать сложные конфигурации через JSON-структуры:

ajv.addKeyword({
  keyword: "range",
  type: "number",
  validate: function (schema, data) {
    return data >= schema.min && data <= schema.max;
  }
});

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

const schema = {
  type: "number",
  range: { min: 10, max: 20 }
};

Такой подход позволяет создавать компактные и выразительные DSL-расширения поверх JSON Schema.

Контекст компиляции и оптимизация

Ajv компилирует схемы в функции, поэтому пользовательские ключевые слова должны быть совместимы с этим процессом. При использовании compile важно учитывать:

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

Оптимизированный вариант:

ajv.addKeyword({
  keyword: "isEven",
  type: "number",
  compile: function () {
    return function (data) {
      return (data & 1) === 0;
    };
  }
});

Такой код выполняется быстрее за счёт битовой операции и отсутствия лишних проверок типов внутри функции.

Встраивание логики трансформации данных

Ключевые слова могут не только валидировать, но и изменять данные:

ajv.addKeyword({
  keyword: "trim",
  type: "string",
  modifying: true,
  compile: function () {
    return function (data, dataPath, parentData, parentDataProperty) {
      if (typeof data === "string") {
        parentData[parentDataProperty] = data.trim();
      }
      return true;
    };
  }
});

Схема:

const schema = {
  type: "string",
  trim: true
};

В этом случае входное значение модифицируется до или во время валидации.

Переиспользуемые фабрики ключевых слов

При работе с большим количеством схожих правил удобно создавать фабрики:

function createMinLengthKeyword(name, minLength) {
  return {
    keyword: name,
    type: "string",
    validate: function (schema, data) {
      return typeof data === "string" && data.length >= minLength;
    }
  };
}

ajv.addKeyword(createMinLengthKeyword("minLen5", 5));
ajv.addKeyword(createMinLengthKeyword("minLen10", 10));

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

Влияние пользовательских ключевых слов на схему

Пользовательские ключевые слова полностью интегрируются в процесс компиляции JSON Schema. Они:

  • участвуют в порядке проверки вместе со стандартными ключевыми словами;
  • могут влиять на ветвление схем;
  • могут быть частью условных конструкций (if, then, else);
  • могут комбинироваться с allOf, anyOf, oneOf.

Пример:

const schema = {
  type: "object",
  properties: {
    age: { type: "number", positive: true },
    score: { type: "number", isEven: true }
  },
  required: ["age", "score"]
};

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

При проектировании расширений важно учитывать ряд ограничений:

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

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