Обработка промисов

Валидация JSON-схем в Ajv традиционно синхронна, однако современный стек разработки требует поддержки асинхронных проверок: запросов к базе данных, обращений к внешним API, чтения распределённых кэшей. Для этого в библиотеке реализована полноценная модель работы через Promise, позволяющая встраивать асинхронную логику прямо в процесс валидации.

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

  • схемы с флагом "$async": true
  • асинхронные пользовательские ключевые слова и форматы
  • компиляция через compileAsync

Схемы с поддержкой $async

Флаг "$async": true включает режим, при котором валидатор всегда возвращает Promise. Любая ошибка приводит к отклонению промиса с объектом Ajv.ValidationError.

Пример схемы:

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

Компиляция такой схемы:

import Ajv from "ajv";

const ajv = new Ajv();

const validate = ajv.compile(schema);

validate({ userId: "123" })
  .then(data => {
    console.log("Данные валидны", data);
  })
  .catch(err => {
    console.log("Ошибка валидации", err);
  });

Особенность заключается в том, что даже если внутри схемы нет асинхронных операций, результат всё равно оборачивается в Promise, обеспечивая единый интерфейс обработки.

Компиляция через compileAsync

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

const validate = await ajv.compileAsync(schema);

Этот метод возвращает Promise, который резолвится функцией-валидатором. Внутренне Ajv анализирует наличие $async и асинхронных зависимостей.

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

try {
  const validate = await ajv.compileAsync(schema);

  const result = await validate(payload);
  console.log(result);
} catch (e) {
  console.error(e);
}

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

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

Одной из наиболее мощных возможностей Ajv является расширение через addKeyword. Поддержка async позволяет включать проверку, например, существования пользователя в базе данных.

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

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

const schema = {
  $async: true,
  type: "string",
  userExists: true
};

При валидации:

await validate("user-123");

Если пользователь не найден, промис будет отклонён.

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

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

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

ajv.addFormat("emailExists", {
  async: true,
  validate: async (email) => {
    const exists = await emailService.check(email);
    return exists;
  }
});

Схема:

const schema = {
  $async: true,
  type: "string",
  format: "emailExists"
};

Форматы выполняются на уровне типовой проверки и интегрируются в общий pipeline валидации.

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

При использовании Promise-ориентированной модели Ajv формирует структурированное исключение ValidationError, содержащее:

  • errors — массив ошибок
  • dataPath — путь к некорректному значению
  • schemaPath — путь к правилу схемы
  • params — дополнительные параметры контекста

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

try {
  await validate(data);
} catch (err) {
  for (const error of err.errors) {
    console.log(error.instancePath, error.message);
  }
}

Важно, что отклонение промиса происходит только при invalid результате. Успешная валидация возвращает исходные данные или undefined в зависимости от конфигурации.

Параллельные проверки и управление промисами

При проектировании сложных систем валидации часто требуется запуск нескольких независимых проверок. Хотя Ajv сам управляет последовательностью асинхронных шагов внутри схемы, внешняя параллелизация возможна через Promise.all.

const validators = [
  ajv.compileAsync(schemaUser),
  ajv.compileAsync(schemaProfile),
  ajv.compileAsync(schemaPermissions)
];

const [v1, v2, v3] = await Promise.all(validators);

await Promise.all([
  v1(userData),
  v2(profileData),
  v3(permissionData)
]);

Такой подход снижает латентность при валидации независимых сущностей.

Асинхронная валидация в потоках данных

В системах обработки событий (например, очереди сообщений или стримы) асинхронные валидаторы применяются как промежуточные фильтры.

async function processMessage(msg) {
  try {
    await validate(msg.payload);
    await handle(msg.payload);
  } catch (e) {
    await sendToDeadLetterQueue(msg);
  }
}

Ajv в этом контексте выступает как первый слой защиты данных, предотвращающий попадание неконсистентных структур в бизнес-логику.

Особенности жизненного цикла асинхронной схемы

Асинхронные схемы требуют более строгого контроля состояния:

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

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

Интеграция с серверными фреймворками

Асинхронная модель Ajv естественно интегрируется в middleware-подход.

app.post("/api", async (req, res) => {
  try {
    await validate(req.body);
    res.send({ ok: true });
  } catch (e) {
    res.status(400).send(e.errors);
  }
});

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

Комбинирование синхронных и асинхронных правил

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

const schema = {
  $async: true,
  type: "object",
  properties: {
    id: { type: "string" },
    email: { format: "emailExists" }
  },
  required: ["id", "email"]
};

Синхронные проверки выполняются немедленно, асинхронные становятся частью цепочки промисов, формируя единый поток валидации.

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

Использование Promise добавляет накладные расходы, однако Ajv оптимизирует выполнение за счёт:

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

Основная стоимость возникает не в Ajv, а в пользовательских асинхронных операциях (HTTP, DB, I/O).

Поведение при ошибках внутри async-валидаторов

Если асинхронная функция выбрасывает исключение, оно интерпретируется как ошибка валидации:

validate: async () => {
  throw new Error("DB failure");
}

В этом случае Ajv преобразует исключение в ValidationError, сохраняя контекст схемы и ключевого слова.

Такой механизм обеспечивает единообразную обработку как бизнес-ошибок, так и технических сбоев.