Асинхронные форматы

Валидация JSON-схем в Ajv изначально ориентирована на синхронную проверку данных, однако современная практика валидации часто требует обращения к внешним источникам: базам данных, HTTP API, сервисам авторизации. Это приводит к необходимости асинхронных форматов и расширений схемы.

Модель асинхронной валидации в Ajv

Асинхронная валидация в Ajv реализуется через возврат Promise из функции проверки. При наличии хотя бы одного асинхронного элемента вся функция валидации становится асинхронной.

import Ajv from "ajv";

const ajv = new Ajv();

const schema = {
  type: "object",
  properties: {
    username: { type: "string" }
  },
  required: ["username"]
};

const validate = ajv.compile(schema);

const data = { username: "test_user" };

validate(data)
  .then(valid => {
    if (!valid) console.log(validate.errors);
  })
  .catch(err => console.error(err));

Асинхронность активируется не на уровне схемы как флаг, а через использование асинхронных пользовательских расширений: форматов или кастомных ключевых слов.

Асинхронные форматы и их поведение

Форматы в Ajv используются через ключ format. В стандартной модели формат — синхронная функция проверки строки или числа. Однако при расширенной конфигурации возможно использование асинхронных форматов, если формат определён как функция, возвращающая Promise<boolean>.

const ajv = new Ajv({ allErrors: true });

ajv.addFormat("username-available", async (value) => {
  const response = await fetch(`https://api.example.com/check?u=${value}`);
  const result = await response.json();
  return result.available === true;
});

const schema = {
  type: "string",
  format: "username-available"
};

const validate = ajv.compile(schema);

validate("john_doe").then(valid => {
  console.log(valid);
});

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

Ограничения форматов при асинхронности

Ключевое ограничение формата в Ajv заключается в том, что:

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

Это приводит к архитектурному следствию: схема перестаёт быть “быстрой локальной проверкой” и становится частью I/O процесса.

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

Более гибкий и предсказуемый механизм — пользовательские ключевые слова. Они изначально поддерживают асинхронность и позволяют разделять ответственность.

ajv.addKeyword({
  keyword: "usernameAvailable",
  async: true,
  type: "string",
  validate: async function check(schema, data) {
    const res = await fetch(`https://api.example.com/check?u=${data}`);
    const json = await res.json();
    return json.available;
  }
});

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

Такой подход предпочтительнее форматов при сложной логике, поскольку:

  • не нарушается семантика format;
  • явно выражается бизнес-правило;
  • упрощается тестирование.

Интеграция с внешними API

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

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

Пример с имитацией базы данных:

const fakeDB = new Set(["admin", "root", "system"]);

ajv.addFormat("unique-username", async (value) => {
  await new Promise(r => setTimeout(r, 50));
  return !fakeDB.has(value);
});

Производительность асинхронных форматов

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

  • каждая проверка может стать сетевым запросом;
  • параллелизм ограничен количеством Promise;
  • отсутствует возможность полного кеширования на уровне Ajv.

Для снижения нагрузки применяются стратегии:

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

Кэширование результатов проверок

Типовой подход — мемоизация результатов асинхронного формата:

const cache = new Map();

ajv.addFormat("cached-username", async (value) => {
  if (cache.has(value)) return cache.get(value);

  const result = await fetch(`https://api.example.com/check?u=${value}`)
    .then(r => r.json());

  cache.set(value, result.available);
  return result.available;
});

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

Ошибки и обработка отказов

Асинхронные форматы усложняют обработку ошибок:

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

Пример обработки таймаута:

ajv.addFormat("safe-remote-check", async (value) => {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), 1000);

  try {
    const res = await fetch("https://api.example.com/check", {
      signal: controller.signal
    });
    const json = await res.json();
    return json.ok === true;
  } finally {
    clearTimeout(timeout);
  }
});

Архитектурные особенности использования асинхронных форматов

Асинхронные форматы меняют роль схемы:

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

В крупных системах это приводит к разделению:

  • локальные схемы (без async) — для UI и быстрых проверок;
  • серверные схемы (async) — для бизнес-ограничений;
  • гибридные схемы — комбинирующие оба подхода.

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