Валидация JSON-схем в Ajv изначально ориентирована на синхронную проверку данных, однако современная практика валидации часто требует обращения к внешним источникам: базам данных, HTTP API, сервисам авторизации. Это приводит к необходимости асинхронных форматов и расширений схемы.
Асинхронная валидация в 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 заключается в том, что:
Это приводит к архитектурному следствию: схема перестаёт быть “быстрой локальной проверкой” и становится частью 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;Асинхронные форматы часто используются для следующих сценариев:
Пример с имитацией базы данных:
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;Для снижения нагрузки применяются стратегии:
Типовой подход — мемоизация результатов асинхронного формата:
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;
});
Это снижает количество обращений к внешним сервисам, но требует контроля устаревания данных.
Асинхронные форматы усложняют обработку ошибок:
Пример обработки таймаута:
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);
}
});
Асинхронные форматы меняют роль схемы:
В крупных системах это приводит к разделению:
Такой подход снижает задержки пользовательского интерфейса и сохраняет строгую проверку данных на серверной стороне.