JSON Schema validation with first-class TypeScript and zero runtime cost. AOT compile your schemas to per-schema ESM modules with no validator dependency. Validator<T> composes with TypeBox, Zod-from-JSON-Schema, Valibot, or hand-written types. Runtime API available for dynamic schemas.
npm install --save-dev ata-validator
npx ata build 'schemas/*.json' --out-dir src/generatedIn your code:
import { validate, isValid, type User } from './generated/user.compiled.mjs'
if (isValid(req.body)) {
const user: User = req.body
// ...
}The .compiled.mjs modules are self-contained: zero runtime dependency on ata-validator, fully tree-shakeable, with TypeScript types emitted alongside.
| Dimension | Schema | ata-AOT | AJV-runtime | Difference |
|---|---|---|---|---|
| Bundle (gzipped) | simple | 955 B | 52.7 KB | 56x smaller |
| Bundle (gzipped) | complex | 1.6 KB | 52.7 KB | 32x smaller |
| Cold start | simple | 21 ms | 38 ms | 1.8x faster |
| Throughput (10M ops) | simple | 345 Mops/s | 116 Mops/s | 3.0x faster |
| Compile time | simple | 6 µs | 1.5 ms | 246x faster |
Reproduce on your machine with npm run bench:aot-vs-ajv. Numbers measured on Apple M4 Pro, Node 25.2.1.
The wins are largest on bundle size and compile time because AOT moves work from runtime to build time. Throughput and cold start are also faster because the compiled validator is a tight straight-line function with no schema-walk overhead.
ata's error output is compiler-grade: each error carries a stable code, an inline source frame pointing at the schema file, and another pointing at the offending bytes in the request payload. Renderers ship in three styles:
import { Validator, renderPretty, renderCompact, renderJSON } from 'ata-validator'
const v = new Validator(schema, { source: { path: 'schemas/user.json', content: schemaText } })
const r = v.validateJSON(input)
if (!r.valid) {
console.error(renderPretty(r.errors))
// error[ATA3001]: value does not match format "email"
// --> schemas/user.json:5:7
// |
// 5 | "email": { "type": "string", "format": "email" }
// | ^^^^^^^ expected format 'email'
// |
// --> input, byte 23
// |
// 1 | {"name":"M","email":"not-an-email","age":-3}
// | ^^^^^^^^^^^^^^ got "not-an-email"
// |
// = help: missing '@' and domain part
// = note: see https://ata-validator.com/e/ATA3001
}The ata CLI ships ata validate <schema> <data> for one-off checks. TTY auto-renders pretty; pipes default to compact; --format=json produces structured output for tooling.
Errors carry a stable code field (ATA####), see the error code registry. Each code has a permalink at https://ata-validator.com/e/<CODE>.
For consumers who built log dashboards on the v0.14 error shape, new Validator(schema, { richErrors: false }) returns the legacy shape exactly. For high-throughput paths, abortEarly: true continues to short-circuit; the returned error carries code: 'ATA9000' and no enrichment.
ata build is for schemas you know at build time. If your schemas are user-supplied at runtime (form builders, no-code platforms, dynamic API ingestion), use the runtime API:
import { Validator } from 'ata-validator'
const v = new Validator(schema)
const result = v.validate(data)The runtime API is unchanged from previous releases. AJV-shim users continue importing from ata-validator/compat.
const { Validator } = require('ata-validator');
const v = new Validator({
type: 'object',
properties: {
name: { type: 'string', minLength: 1 },
email: { type: 'string', format: 'email' },
age: { type: 'integer', minimum: 0 },
role: { type: 'string', default: 'user' }
},
required: ['name', 'email']
});
// Fast boolean check - JS codegen, 15.3M ops/sec
v.isValidObject({ name: 'Mert', email: '[email protected]', age: 26 }); // true
// Full validation with error details + defaults applied
const result = v.validate({ name: 'Mert', email: '[email protected]' });
// result.valid === true, data.role === 'user' (default applied)
// JSON string validation (simdjson fast path)
v.validateJSON('{"name": "Mert", "email": "[email protected]"}');
v.isValidJSON('{"name": "Mert", "email": "[email protected]"}'); // true
// Buffer input (zero-copy, raw NAPI)
v.isValid(Buffer.from('{"name": "Mert", "email": "[email protected]"}'));
// Parallel batch - multi-core, NDJSON, 13.4M items/sec
const ndjson = Buffer.from(lines.join('\n'));
v.isValidParallel(ndjson); // bool[]
v.countValid(ndjson); // numberata infers TypeScript types straight from plain JSON Schema. Write the schema once with defineSchema, and both runtime validation and the static type come from it, with no builder DSL and no second type declaration to keep in sync.
import { defineSchema, Validator } from 'ata-validator'
const userSchema = defineSchema({
type: 'object',
properties: {
id: { type: 'integer', minimum: 1 },
role: { type: 'string', enum: ['admin', 'user'] },
},
required: ['id'],
})
const v = new Validator(userSchema)
const result = v.validate(data)
if (result.valid) {
result.data.id // number
result.data.role // 'admin' | 'user' | undefined
} else {
// result.errors: ValidationError[]
}defineSchema returns the schema untouched at runtime; in TypeScript it gives keyword autocomplete and an error when a value has the wrong shape, with no as const needed. new Validator(schema) carries the inferred type, so a successful validate narrows result.data with no manual annotation.
You can also pull the type out directly with Infer, with no second declaration to keep in sync.
import { defineSchema, type Infer } from 'ata-validator'
const event = defineSchema({
$defs: {
Point: { type: 'object', properties: { x: { type: 'number' }, y: { type: 'number' } }, required: ['x', 'y'] },
},
type: 'object',
properties: {
kind: { enum: ['click', 'scroll'] },
at: { $ref: '#/$defs/Point' },
path: { type: 'array', prefixItems: [{ type: 'string' }, { type: 'integer' }] },
},
required: ['kind', 'at'],
})
type Event = Infer<typeof event>
// {
// kind: 'click' | 'scroll'
// at: { x: number; y: number }
// path?: [string, number]
// }Infer resolves const/enum to literals, anyOf/oneOf to unions, allOf to intersections, prefixItems to tuples, and local $ref into #/$defs or #/definitions, including recursive references. An external or unresolvable $ref resolves to unknown rather than erroring. The exported JSONSchema type is available if you want to annotate a schema by hand; custom and vendor keywords are allowed. Requires TypeScript >= 5.0.
If you prefer a chainable builder over JSON Schema literals, ata-validator/t ships one whose output is still plain JSON Schema. The runtime validator, Infer<S>, and the AOT pipeline all keep working without an adapter. The migration from TypeBox is one import rename, then the same authoring shape:
import { t } from 'ata-validator/t'
import { Validator, type Infer } from 'ata-validator'
const User = t.object({
id: t.integer(),
name: t.string({ minLength: 1 }),
email: t.optional(t.string({ format: 'email' })),
role: t.union([t.literal('admin'), t.literal('user')]),
})
type User = Infer<typeof User>
// { id: number; name: string; role: 'admin' | 'user'; email?: string }
const v = new Validator(User)The builder covers primitives (string, number, integer, boolean, null), composites (object with optional keys, array, tuple, record, union, intersect, literal, const, enum), and refs (ref). Optionality is carried by a Symbol marker that the emitted JSON Schema and ata's codegen never see, so the output is still a plain JSON Schema literal that you can pass to anything that takes one.
Validator<T> is generic, so if you already author schemas with a library, pass the type and ata narrows to it. No library-specific assumption.
import { Type, type Static } from '@sinclair/typebox'
import { Validator } from 'ata-validator'
const UserSchema = Type.Object({
id: Type.Integer({ minimum: 1 }),
name: Type.String({ minLength: 1 }),
})
const v = new Validator<Static<typeof UserSchema>>(UserSchema)The same works with Zod-from-JSON-Schema, Valibot, or a hand-written type User = {...} alongside a JSON Schema literal.
const addressSchema = {
$id: 'https://example.com/address',
type: 'object',
properties: { street: { type: 'string' }, city: { type: 'string' } },
required: ['street', 'city']
};
const v = new Validator({
type: 'object',
properties: {
name: { type: 'string' },
address: { $ref: 'https://example.com/address' }
}
}, { schemas: [addressSchema] });
// Or use addSchema()
const v2 = new Validator(mainSchema);
v2.addSchema(addressSchema);const v = new Validator(schema, {
coerceTypes: true, // "42" → 42 for integer fields
removeAdditional: true, // strip properties not in schema
schemas: [otherSchema], // cross-schema $ref registry
abortEarly: true, // skip detailed error collection on failure (~4x faster on invalid data)
});abortEarly returns a shared { valid: false, errors: [{ message: 'validation failed' }] } on failure instead of running the detailed error collector. Useful when the caller only needs a pass/fail decision (Fastify route guards, high-throughput gatekeepers, request rejection at the edge).
The ata CLI turns a JSON Schema file into a self-contained JavaScript module. No runtime dependency on ata-validator, so only the generated validator ships to the browser. Typical output is ~1 KB gzipped compared to ~27 KB for the full runtime.
npx ata compile schemas/user.json -o src/generated/user.validator.mjsThe CLI emits two files: the validator itself and a paired .d.mts (or .d.cts) with the inferred TypeScript type plus an isValid type predicate.
import { isValid, validate, type User } from './user.validator.mjs'
const incoming: unknown = JSON.parse(req.body)
if (isValid(incoming)) {
// TypeScript narrows to User here
incoming.id // number
incoming.role // 'admin' | 'user' | 'guest' | undefined
}
const r = validate(incoming)
// { valid: true, errors: [] } | { valid: false, errors: ValidationError[] }CLI options:
| Flag | Default | Description |
|---|---|---|
-o, --output <file> |
<schema>.validator.mjs |
Output path |
-f, --format <fmt> |
esm |
esm or cjs |
--name <TypeName> |
from filename | Root type name in the .d.ts |
--abort-early |
off | Generate the stub-error variant (~0.5 KB gzipped) |
--no-types |
off | Skip the .d.mts / .d.cts output |
For a project with many schemas, ata build <glob> compiles them all in one command:
npx ata build 'schemas/*.json' --out-dir build/validators --checkRun with --watch during development for incremental rebuilds.
Typical bundle sizes (10-field user schema, gzipped):
| Variant | Size | Notes |
|---|---|---|
ata-validator runtime |
~27 KB | Full compiler + all keywords |
ata compile (standard) |
~1.1 KB | Validator + detailed error collector |
ata compile --abort-early |
~0.5 KB | Validator + stub errors only |
Programmatic API if you prefer to script it:
const fs = require('fs');
const { Validator } = require('ata-validator');
const v = new Validator(schema);
fs.writeFileSync('./user.validator.mjs', v.toStandaloneModule({ format: 'esm' }));Fastify startup (10 routes cold): ajv 12.6ms → ata 0.5ms (24x faster boot, no build step required)
const v = new Validator(schema);
// Works with Fastify, tRPC, TanStack, etc.
const result = v['~standard'].validate(data);
// { value: data } on success
// { issues: [{ message, path }] } on failurenpm install fastify-ataconst fastify = require('fastify')();
fastify.register(require('fastify-ata'), {
coerceTypes: true,
removeAdditional: true,
});
// All existing JSON Schema route definitions work as-is#include "ata.h"
auto schema = ata::compile(R"({
"type": "object",
"properties": { "name": {"type": "string"} },
"required": ["name"]
})");
auto result = ata::validate(schema, R"({"name": "Mert"})");
// result.valid == trueCopy-paste recipes for the common frameworks. Most need 10-20 lines of glue. See docs/integrations for the full set.
| Framework | Pattern | Recipe |
|---|---|---|
| Fastify | dedicated plugin | fastify-ata |
| Vite (build-time compile) | dedicated plugin | ata-vite |
| Hono | async middleware | docs/integrations/hono.md |
| Elysia | direct handler check | docs/integrations/elysia.md |
| tRPC | Standard Schema V1 input | docs/integrations/trpc.md |
| TanStack Form | Standard Schema V1 validator | docs/integrations/tanstack-form.md |
| Express | sync middleware | docs/integrations/express.md |
| Koa | async ctx middleware | docs/integrations/koa.md |
| NestJS | validation pipe | docs/integrations/nestjs.md |
| SvelteKit | form action, API route | docs/integrations/sveltekit.md |
| Astro | API route, server action | docs/integrations/astro.md |
| Category | Keywords |
|---|---|
| Type | type |
| Numeric | minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf |
| String | minLength, maxLength, pattern, format |
| Array | items, prefixItems, minItems, maxItems, uniqueItems, contains, minContains, maxContains, unevaluatedItems |
| Object | properties, required, additionalProperties, patternProperties, minProperties, maxProperties, propertyNames, dependentRequired, dependentSchemas, unevaluatedProperties |
| Enum/Const | enum, const |
| Composition | allOf, anyOf, oneOf, not |
| Conditional | if, then, else |
| References | $ref, $defs, definitions, $id |
| Boolean | true, false |
email, date, date-time, time, uri, uri-reference, ipv4, ipv6, uuid, hostname
Native builds require C/C++ toolchain support and the following libraries:
re2abseilmimalloc
Install them before running npm install / npm run build:
# macOS (Homebrew)
brew install re2 abseil mimalloc# Ubuntu/Debian (apt)
sudo apt-get update
sudo apt-get install -y libre2-dev libabsl-dev libmimalloc-dev# C++ library + tests
cmake -B build
cmake --build build
./build/ata_tests
# Node.js addon
npm install
npm run build
npm test
# JSON Schema Test Suite
npm run test:suiteMIT