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 Type | Async Support | How |
|---|---|---|
| macro | ❌ No | Runs at schema resolution before any data exists |
| compile | âś… Yes | Return an async function from the factory |
| validate | âś… Yes | Make the validate function async |
| code | âś… Yes | Use 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;
};
},
});