async/await паттерн

Библиотека Ajv поддерживает работу с JSON Schema как в синхронном, так и в асинхронном режиме. Асинхронность становится необходимой, когда правила валидации зависят от внешних источников данных: базы данных, HTTP-запросов, файловой системы или других сервисов.

Асинхронная модель в Ajv строится на Promise и полностью совместима с async/await, что позволяет интегрировать валидацию в современный поток выполнения JavaScript-кода без колбэков и дополнительных обёрток.


Promise как базовая единица результата валидации

При использовании асинхронных схем результат работы валидатора перестаёт быть булевым значением. Вместо этого функция валидации возвращает Promise, который:

  • разрешается (resolve) при успешной валидации;
  • отклоняется (reject) или возвращает false через результат с ошибками при невалидных данных.

Базовый пример асинхронной проверки:

import Ajv from "ajv";

const ajv = new Ajv();

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

const validate = ajv.compile(schema);

async function run(data) {
  const valid = await validate(data);

  if (!valid) {
    console.log(validate.errors);
  }
}

Хотя пример выглядит синхронным, переход к async происходит автоматически, когда в схеме появляются асинхронные ключевые слова или пользовательские async-функции.


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

Основной механизм асинхронности в Ajv реализуется через пользовательские keywords.

Асинхронный keyword определяется как функция, возвращающая Promise:

ajv.addKeyword({
  keyword: "isExistingUser",
  async: true,
  type: "string",
  validate: async function (schema, data) {
    const user = await database.findUserById(data);
    return Boolean(user);
  }
});

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

const schema = {
  type: "object",
  properties: {
    userId: { isExistingUser: true }
  }
};

Поведение async keyword

  • выполнение полностью приостанавливает pipeline валидации до завершения Promise;
  • результат влияет на общий итог валидации;
  • ошибки могут быть сгенерированы через throw new Error() внутри async-функции.

Компиляция асинхронных схем

Ajv предоставляет метод compileAsync, который гарантирует корректную обработку схем, содержащих асинхронные элементы:

const validate = await ajv.compileAsync(schema);

const valid = await validate(data);

Особенности:

  • используется только когда схема содержит async keywords;
  • возвращает Promise вместо функции;
  • ускоряет повторное использование валидатора после компиляции.

Смешанные схемы: синхронные и асинхронные проверки

В одной схеме могут одновременно присутствовать:

  • синхронные проверки типов (type, minLength, pattern);
  • асинхронные проверки (custom keywords, внешние API).

Пример:

ajv.addKeyword({
  keyword: "isUniqueEmail",
  async: true,
  validate: async function (schema, email) {
    const exists = await api.checkEmail(email);
    return !exists;
  }
});

const schema = {
  type: "object",
  properties: {
    email: {
      type: "string",
      format: "email",
      isUniqueEmail: true
    }
  }
};

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


Паттерн async/await при валидации данных

Использование async/await упрощает контроль потока обработки данных, особенно в сервисах API:

async function createUser(data) {
  const valid = await validate(data);

  if (!valid) {
    throw new Error("Validation failed");
  }

  return await database.saveUser(data);
}

Особенности паттерна:

  • валидация становится частью линейного потока;
  • исключается необходимость callback-цепочек;
  • ошибки легко обрабатываются через try/catch.

Асинхронные проверки с внешними API

Частый сценарий — проверка уникальности, существования или доступности ресурсов.

ajv.addKeyword({
  keyword: "existsInService",
  async: true,
  validate: async function (schema, value) {
    const response = await fetch(`https://api.service.com/check/${value}`);
    const result = await response.json();

    return result.exists === true;
  }
});

Такой подход делает схему зависимой от внешних систем, что требует учёта:

  • задержек сети;
  • возможных таймаутов;
  • кэширования результатов.

Кэширование результатов асинхронной валидации

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

const cache = new Map();

ajv.addKeyword({
  keyword: "cachedCheck",
  async: true,
  validate: async function (schema, value) {
    if (cache.has(value)) {
      return cache.get(value);
    }

    const result = await expensiveCheck(value);
    cache.set(value, result);

    return result;
  }
});

Кэш снижает нагрузку на внешние сервисы и уменьшает latency валидации.


Ошибки в асинхронной валидации

Ошибки в async-логике могут возникать на нескольких уровнях:

1. Ошибка Promise

validate: async () => {
  throw new Error("Service unavailable");
}

2. Ошибка сети

Возникает при использовании fetch или других HTTP-клиентов.

3. Некорректное значение return

Асинхронная функция обязана возвращать boolean или Promise. Возврат других типов приводит к непредсказуемому результату валидации.


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

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

  • увеличивает latency из-за I/O операций;
  • блокирует pipeline до завершения Promise;
  • требует осторожного проектирования порядка проверок.

Оптимизация достигается за счёт:

  • раннего выполнения дешёвых sync-проверок;
  • минимизации числа async keywords;
  • кэширования внешних запросов;
  • параллельных запросов внутри одного keyword.

Параллелизм внутри async keyword

Возможна агрегация нескольких асинхронных проверок:

validate: async (schema, value) => {
  const [a, b] = await Promise.all([
    serviceA.check(value),
    serviceB.check(value)
  ]);

  return a && b;
}

Это снижает общее время валидации по сравнению с последовательными вызовами.


Интеграция с обработкой ошибок приложения

Асинхронная валидация естественно интегрируется в архитектуру сервисов:

async function handler(req, res) {
  try {
    const valid = await validate(req.body);

    if (!valid) {
      return res.status(400).json(validate.errors);
    }

    const result = await process(req.body);
    res.json(result);
  } catch (err) {
    res.status(500).json({ message: err.message });
  }
}

Такой подход объединяет:

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

Композиция асинхронных схем

Ajv позволяет строить модульные схемы, где async-валидация используется как отдельные блоки:

const userSchema = {
  type: "object",
  properties: {
    email: { type: "string", isUniqueEmail: true },
    id: { isExistingUser: true }
  }
};

Композиция облегчает повторное использование логики и изоляцию внешних зависимостей.


Контроль выполнения async pipeline

Ajv выполняет асинхронную валидацию по внутреннему графу зависимостей схемы:

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

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