Skip to Content
@jetio/validator docs are live 🎉

Async Validation

Three of the four keyword types support async validation. This page covers how to enable it correctly and avoid race conditions.


Which Keywords Support Async?

Keyword TypeAsync SupportHow
macro❌ NoRuns at schema resolution before any data exists
compileâś… YesReturn an async function from the factory
validateâś… YesMake the validate function async
codeâś… YesUse await directly in the generated code string

⚠️ Critical: Enable Async Mode

You must pass async: true to the @jetio/validator constructor or compiler when using async keywords. Without it, validation returns immediately before async operations complete, producing incorrect results.

// ❌ WRONG — race conditions const validator = new JetValidator(); validator.addKeyword({ keyword: "checkDB", validate: async (sv, data) => { ... } }); const validate = validator.compile(schema); const result = validate({ id: "test" }); // returns before async completes! // ✅ CORRECT — instance level const validator = new JetValidator({ async: true }); // ✅ CORRECT — per compilation const validator = new JetValidator(); const validate = validator.compile(schema, { async: true });

Async with compile

Return an async function from the factory:

const validator = new JetValidator({ async: true }); validator.addKeyword({ keyword: "uniqueEmail", type: "string", compile: (config) => { return async (data, rootData, dataPath) => { const exists = await checkEmailInDatabase(data); if (exists) return { message: "Email already exists" }; return true; }; }, }); const validate = validator.compile(schema); const result = await validate({ email: "test@example.com" });

Async with validate

Make the validate function itself async:

const validator = new JetValidator({ async: true }); validator.addKeyword({ keyword: "existsInAPI", type: "string", validate: async (schemaValue, data) => { if (!schemaValue) return true; const response = await fetch(`https://api.example.com/check/${data}`); const result = await response.json(); if (!result.exists) return { message: `ID '${data}' does not exist` }; return true; }, }); const validate = validator.compile(schema); const result = await validate({ id: "user-123" });

Async with code

Use await directly in the generated code string. The async property on the keyword definition does nothing — async behavior comes from the instance or compile config:

validator.addKeyword({ keyword: "asyncCheck", type: "string", code: (schemaValue, parentSchema, context) => { return ` const _res = await fetch(${context.dataVar}); await _res.json(); `; }, }); // Enable async at instance or compile level const validator1 = new JetValidator({ async: true }); const validate1 = validator1.compile(schema); const validator2 = new JetValidator(); const validate2 = validator2.compile(schema, { async: true });

Multiple Async Keywords

All async keywords in the same schema work correctly as long as async: true is enabled:

const validator = new JetValidator({ async: true, allErrors: true }); validator.addKeyword({ keyword: "uniqueEmail", type: "string", compile: (config) => { return async (data) => { const exists = await checkEmailInDatabase(data); return !exists || { message: "Email already registered" }; }; }, }); validator.addKeyword({ keyword: "validUsername", type: "string", validate: async (schemaValue, data) => { const available = await checkUsernameAvailability(data); return available || { message: "Username not available" }; }, }); const schema = { type: "object", properties: { email: { type: "string", uniqueEmail: { apiUrl: "https://api.example.com" }, }, username: { type: "string", validUsername: true }, }, }; const validate = validator.compile(schema); const result = await validate({ email: "user@example.com", username: "johndoe", });

Best Practices

Always enable async: true when you have any async keywords:

const validator = new JetValidator({ async: true, allErrors: true, }); // or per-compilation validator.compile(schema, { async: true });

Handle errors inside async operations:

validator.addKeyword({ keyword: "apiCheck", validate: async (schemaValue, data) => { try { const result = await apiCall(data); return result.valid || { message: result.error }; } catch (error) { return { message: "API validation failed", details: error.message }; } }, });

Add timeouts:

validator.addKeyword({ keyword: "timedCheck", compile: (config) => { return async (data) => { const timeout = config.timeout || 5000; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); try { const response = await fetch(config.url, { signal: controller.signal }); clearTimeout(timeoutId); const result = await response.json(); return result.valid || { message: "Validation failed" }; } catch (error) { clearTimeout(timeoutId); if (error.name === "AbortError") { return { message: `Validation timeout after ${timeout}ms` }; } return { message: "Validation error", details: error.message }; } }; }, });

Cache repeated async validations:

const cache = new Map(); validator.addKeyword({ keyword: "cachedCheck", compile: (config) => { return async (data) => { if (cache.has(data)) return cache.get(data); const exists = await checkDatabase(data); const result = exists || { message: "Not found" }; cache.set(data, result); return result; }; }, });
Last updated on