Two additions that round out the validation surface: custom error messages and async refinement.
Custom error messages
A subschema can now carry an errorMessage keyword to override the message on the errors it produces. A string replaces the message for any failing keyword on that subschema; an object overrides per keyword, with required keyed by the missing property name (or a single string) and _ as a fallback. The code, keyword, and path fields are untouched, so existing renderers and log dashboards keep working. The override only runs when a schema actually declares errorMessage, so schemas without it pay nothing.
const v = new Validator({
type: 'object',
properties: {
age: { type: 'integer', minimum: 18, errorMessage: { minimum: 'must be 18 or older' } },
},
required: ['age'],
errorMessage: { required: { age: 'age is required' } },
})
v.validate({ age: 5 }).errors[0].message // 'must be 18 or older'Async refinement
JSON Schema is synchronous, so checks that need to await (a uniqueness lookup, a remote call, a cross-field rule) attach to a schema through t.refine on the ata-validator/t builder and run via the new validateAsync / parseAsync. The refinement rides on a Symbol marker that the emitted JSON Schema and the codegen never see, so new Validator(schema) still does plain structural validation and ignores it. validateAsync runs the structural pass first and awaits the refinements only when the value is structurally valid, so a refinement body can assume the right shape.
import { t } from 'ata-validator/t'
import { validateAsync } from 'ata-validator'
const Signup = t.refine(
t.object({ username: t.string({ minLength: 3 }) }),
async (value) => !(await usernameTaken(value.username)),
{ message: 'username is already taken', path: '/username' },
)
const r = await validateAsync(Signup, body)Refinements compose by wrapping again, a check may be sync or async, and a failure surfaces as an error with keyword: 'refine' carrying the supplied message and path. parseAsync resolves to the validated value or throws with the errors attached.
Both features keep the existing validate path synchronous and allocation-free, and neither changes behaviour for schemas that do not use them.