ata-validator
JSON Schema validation that compiles for speed and still runs where code generation is blocked. The compiled and interpreted engines answer identically, at 100% of the official suite in both modes, across Draft 2020-12, draft 7 and the JSON Schema v1 dialect. First-class TypeScript inference, and ata build emits a standalone module that imports nothing.

1.0 is a stability commitment: see docs/STABILITY.md for the semver, deprecation, and error-code guarantees.
Who uses ata
Projects whose code depends on ata, from GitHub's dependency graph, and the framework that lists
its plugin, with what each does with it:
- Socket uses ata, compiled ahead of time, in the shared build
tooling of its repositories.
- Fastify lists the
fastify-ata plugin in its ecosystem.
- react-jsonschema-form ships an ata
validator in its main repository, in a runtime and a precompiled form.
- JollyPixel parses and validates JSON on its back end
with ata, compiled ahead of time.
- better-drizzle ships an ata plugin for queries
and rows.
- svelte-jsonschema-form publishes an ata
validator package, runtime and precompiled.
Quick start
npm install --save-dev ata-validator
npx ata build 'schemas/*.json' --out-dir src/generated
The ata-validator package itself is pure JavaScript. The native accelerator (simdjson parsing, parallel NDJSON, buffer APIs) ships as per-platform optional packages that npm installs automatically where they fit, the same pattern Vite uses for esbuild. Seven targets are built: macOS on arm64 and x64, Linux on x64 and arm64 against both glibc (2.17 or later, below the 2.28 Node itself needs) and musl, and Windows on x64. A platform without a prebuild still installs and validates, on the pure-JS engine. For a guaranteed zero-binary install:
npm install ata-validator --omit=optional
or set ATA_NO_NATIVE=1 at runtime. Typical schemas compile to specialized JS; shapes the compiler cannot represent (some $dynamicRef, cyclic $ref, unusual keyword interactions) fall back to an interpreted engine, so every schema validates in every environment. The pure-JS setup scores the same on the official suite as the native one, 1301 of 1301 Draft 2020-12 cases. The buffer and parallel APIs (isValid on raw buffers, isValidPrepadded, isValidNDJSON, isValidParallel, countValid, batchIsValid, validateAndParse) work without the addon too, answering through the same checks as isValidJSON(), slower than the addon: 177 ns against 92 for a small document on Node 25. In a browser, which has no Buffer, they throw and name the methods to use instead.
Those four now agree with validate() on every case of the official suite, 3365 across three dialects. The native walker behind them does not handle every shape (contains, unevaluatedProperties, patternProperties, tuple items, cross-document $ref, a few formats), so for schemas using one of those the buffer APIs parse the bytes and answer through validate(); the list is in lib/buffer-gate.js. Typical request schemas stay on the zero-copy path. npm test holds the disagreement count at zero.
Where new Function is refused altogether, on Cloudflare Workers, Deno Deploy or under a strict Content-Security-Policy, ata drops to the interpreted engine and scores the same 1301 of 1301 with code generation blocked. No flags, and on Workers no nodejs_compat either. See docs/edge-runtimes.md.
Node has a switch for exactly that environment, so this takes thirty seconds to check for yourself, on ata or on whatever you use today:
node --disallow-code-generation-from-strings -e "
const { Validator } = require('ata-validator')
const v = new Validator({ type: 'object', properties: { id: { type: 'integer' } }, required: ['id'] })
console.log(v.validate({ id: 1 }).valid, v.validate({ id: 'x' }).valid)
"
A validator that reaches its speed by generating source and calling new Function cannot run under that flag at all, which is the same reason it cannot run on a Worker. That is a deliberate trade rather than an oversight, and it is worth knowing which side of it your validator is on before you deploy to an edge runtime.
In 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.
Code already written with new Validator(schema) gets the same result from the bundler plugin, without changes: @ata-project/unplugin replaces each call whose schema is known at build time with a compiled validator that answers the same, and the runtime leaves the bundle. For the plugin's three-schema test entry a minified Vite build goes from 122.5 KB to 16.7 KB gzipped on ata 1.39.2. Schemas that arrive at runtime keep the runtime, which is the right tool for them.
Measured by others
Public harnesses run ata without ata's involvement. Quote these before anything in this
file.
- The runtime type benchmark
maintained by moltar, 44 to 56 libraries per test. In its official run on Node 24, with ata
1.32.0, ata is first of 45 on assertStrict at 37.2M operations a second, and its
ahead-of-time entry is first of 44 on parseStrict at 36.3M, with the runtime entry second.
On assertLoose ata is fifth of 56 and on parseSafe sixth of 45. Some of the entries ahead
of it on those two rows skip checks JSON Schema requires there, such as rejecting an array
where an object is declared or NaN where a number is. On Deno the same run puts ata between
12th and 23rd; 1.32.1 makes it about 2.5 times faster there, locally, and had not been
through the harness yet at the time of writing.
- schemabenchmarks.dev, a benchmark of runtime validation
libraries. Its published run of 2026-09-25 still uses ata 1.29.0 and puts it sixth of 30 on
valid data at 870 ns and tenth on invalid data at 67 ns. Most of that time was the
uri
format check and a wrapper in the keywords package, both rewritten since. Run locally on an
Apple M4 Pro, the same harness takes ata from 365 ns on 1.29.0 to 240 ns on 1.32.1 for valid
data, with invalid data at 21 ns on both; those are local figures, not the site's.
- Bowtie, the cross-implementation JSON Schema test harness. ata's
harness runs Draft 2020-12 and draft 7 there; on the harness at ata 1.16.1 the official suite
passes with nothing failed, errored or skipped under Bowtie's own runner. The v1 dialect is
declared in a pending harness change that waits for a Bowtie release.
Why AOT
| Bundle (gzipped) | simple | 1.3 KB | 58.1 KB | 43.9x smaller |
| Bundle (gzipped) | complex | 7.8 KB | 58.1 KB | 7.4x smaller |
| Bundle (gzipped) | nested | 4.5 KB | 58.1 KB | 12.9x smaller |
| Cold start | simple | 22 ms | 40 ms | 1.8x faster |
| Throughput (1M ops) | simple | 269 Mops/s | 116 Mops/s | 2.3x faster |
| Compile time | simple | 19 µs | 1.63 ms | 85x faster |
The runtime column is the default validator most frameworks ship. Reproduce on your machine
with npm run bench:aot-vs-ajv. Numbers from one run on Apple M4 Pro, Node 25.2.1, 2026-09-28,
on ata-validator 1.36.0. Across four runs throughput moved between 213 and 269 Mops/s against
94 to 116, cold start between 21 and 22 ms against 40 to 42, and the compile ratio between 79x
and 90x. The throughput row times a single one-million-call
loop of a few milliseconds, so it moves the most from run to run; treat the last three rows as
an order of magnitude rather than a constant.
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.
What the table does not cover is data that fails. It measures compiled modules on input that
passes, and a rejection costs more than a verdict: ata builds an error carrying a code, the
offending value, a documentation link and a suggestion. On a five-field object schema a
passing payload costs about 17 ns and a rejected one about 155 ns. abortEarly: true or
isValidObject() skips that work when only the verdict matters. Schemas ata declines to
compile, mostly cross-document $ref, $dynamicRef and the harder unevaluated* shapes,
run on the interpreted engine. That engine compiles each schema into a tree of closures, so
on a six-field object schema a passing payload costs about 136 ns against 8 ns for the same
shape on the compiled path, both measured warm in one run on the same machine.
Schemas and models
If you ask a model for structured output and validate the result, the schema is
part of the prompt and the validation error is part of the retry. Two functions
produce those strings.
import { describeSchema, toRetryMessage, Validator } from 'ata-validator'
describeSchema(schema)
const r = new Validator(schema).validate(fromTheModel)
if (!r.valid) toRetryMessage(r.errors)
Whether this is worth anything depends on the schema, and the measurement that
says so is
public, with the
harness and the raw results.
On constraints a model can work out from the document, a pattern, a date format,
a currency in uppercase, it changes nothing: 40 of 40 documents recovered on one
retry with the conventional error text, and 40 of 40 with the detailed one.
On constraints it cannot work out, an enum of internal codes while the
document says "paid in part" and "Hamburg office", the conventional text
recovered 0 of 30 and the detailed text 30 of 30. The model does not give up
when it is not told the values, it invents plausible ones, so no number of
retries closes it.
Stating the constraints up front does the same job earlier. Across four prompts
on the same fixtures, first attempts that validate: nothing 0 of 30, a lean
field list 0 of 30, a careful hand-written description 30 of 30 on the inferable
fixture and 0 of 30 on the opaque one, and describeSchema 30 and 23. A person
writing prose matches the generated description wherever prose works. It is the
enumerated values a person leaves out.
One model, Claude Haiku 4.5, one schema shape, 30 to 40 documents per arm, no
temperature control. Read it as a finding, not a law.
Error messages
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))
}
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>.
Custom messages
A subschema can override the human-facing message with an errorMessage keyword. 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 dashboards and renderers keep working.
const v = new Validator({
type: 'object',
properties: {
age: { type: 'integer', minimum: 18, errorMessage: { minimum: 'must be 18 or older', type: 'age has to be a number' } },
email: { type: 'string', format: 'email', errorMessage: 'enter a valid email address' },
},
required: ['email'],
errorMessage: { required: { email: 'email is required' } },
})
v.validate({ age: 5 }).errors[0].message
v.validate({}).errors[0].message
Schemas without an errorMessage keyword pay nothing: the override pass is only installed when one is present.
Opting out
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.
Which path, and where
There are two ways to run ata and they suit different places. Measuring the wrong one
is the most common way to get a misleading number out of this library.
| In a bundle, gzipped | 2.1 KB | 113.3 KB |
| Time to a served request | 3.5 ms | 8.5 ms |
| Schema known when | build time | any time |
The bundle row is the ten-field user schema in
tests/fixtures/error-dx/user.schema.json, every export of the compiled module against
new Validator(schema), built with bun build --minify --target=browser on ata 1.45.0.
The startup row is a Hono route on Bun 1.4, the median of three rounds of best-of-seven, from
benchmark/bundle, against 3.7 ms for the same app doing no validation at all, so the
compiled path costs nothing measurable to start. The runtime figure is what it is because
a schema that arrives at run time can use any keyword, so the whole engine has to be
there. The compiled module imports nothing and contains only the checks your schema asks
for.
On a server, use whichever fits your schemas. 113 KB of JavaScript on a Node or Bun
process is not a cost anyone notices, and the runtime API is the simpler thing to reach
for. Speed is the same either way once warm.
In a browser, on an edge runtime, or anywhere cold starts are charged, compile. This
is where the difference is the whole story, and it is also where new Function is often
blocked outright, which the compiled module does not need. With a bundler, adding @ata-project/unplugin
is enough: it compiles new Validator(schema) calls at build time, and leaves the rest alone.
When to use the runtime API instead
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. Code written against the default validator's class keeps working through ata-validator/compat, which covers compile, addSchema, addFormat, addKeyword, errorsText and the rest of that surface, and reports errors in the same shape and order. npx ata migrate reports what the switch would change in a project, and what it cannot translate, before anything is written. See docs/migration-from-ajv.md.
Usage
Node.js
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']
});
v.isValidObject({ name: 'Mert', email: 'mert@example.com', age: 26 });
const result = v.validate({ name: 'Mert', email: 'mert@example.com' });
v.validateJSON('{"name": "Mert", "email": "mert@example.com"}');
v.isValidJSON('{"name": "Mert", "email": "mert@example.com"}');
v.isValid(Buffer.from('{"name": "Mert", "email": "mert@example.com"}'));
v.engine();
const ndjson = Buffer.from(lines.join('\n'));
v.isValidParallel(ndjson);
v.countValid(ndjson);
Type-safe schemas
ata 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
result.data.role
} else {
}
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>
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.
Chainable authoring with ata-validator/t
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>
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), plus the TypeBox-style modifiers pick, omit, partial, required, composite, and recursive. 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.
const User = t.object({ id: t.integer(), name: t.string(), email: t.optional(t.string()) })
const Patch = t.partial(t.omit(User, ['id']))
type Patch = Infer<typeof Patch>
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 with t.refine and run through validateAsync. The refinement rides on a Symbol marker, so new Validator(schema) still does plain structural validation and ignores it; only validateAsync/parseAsync evaluate it, and only after the value is structurally valid.
import { t } from 'ata-validator/t'
import { validateAsync, parseAsync } from 'ata-validator'
const Signup = t.refine(
t.object({ username: t.string({ minLength: 3 }), email: t.string({ format: 'email' }) }),
async (value) => !(await usernameTaken(value.username)),
{ message: 'username is already taken', path: '/username' },
)
const r = await validateAsync(Signup, body)
if (!r.valid) return reply.code(400).send(r.errors)
const user = await parseAsync(Signup, body)
Refinements compose by wrapping again, and a failing one surfaces as an error with keyword: 'refine' carrying your message and path. A check may be sync or async.
Composes with TypeBox, Zod, or your own types
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.
Cross-Schema $ref
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] });
const v2 = new Validator(mainSchema);
v2.addSchema(addressSchema);
Options
const v = new Validator(schema, {
coerceTypes: true,
removeAdditional: true,
schemas: [otherSchema],
abortEarly: true,
engine: 'interpreter',
});
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).
engine: 'interpreter' keeps one validator off code generation: no new Function, no shared compile cache, the interpreted engine answers every call. The default turns a schema into JavaScript source, which is the right trade for a schema you wrote; a schema that arrives at runtime from a plugin or a tenant is input, and this option validates against it without ever executing anything derived from it. Same verdict, higher per-call cost; ATA_FORCE_NAPI=1 does the same for a whole process.
Build-time compile (ata compile)
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. For the ten-field user schema in tests/fixtures/error-dx/user.schema.json the module is 2.1 KB gzipped, full error detail included, against 113.3 KB for the runtime bundled for the browser (ata 1.45.0).
npx ata compile schemas/user.json -o src/generated/user.validator.mjs
The 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)) {
incoming.id
incoming.role
}
const r = validate(incoming)
CLI options:
-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, about a third of the source |
--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 --check
Run with --watch during development for incremental rebuilds.
Bundle sizes for the 10-field user schema in tests/fixtures/error-dx/user.schema.json,
minified and gzipped, measured with bun build --minify --target=browser on ata 1.45.0:
Validator from ata-validator | 113.3 KB | The compiler ships with it, because a runtime schema can use any keyword |
isValid from the compiled module | 1.3 KB | Nothing else is reachable, so the error collector is dropped |
validate from the compiled module | 1.9 KB | Adds the detailed error collector |
--abort-early makes the generated source about three times smaller, and after
bundling it makes no difference: importing only isValid already leaves the error
collector unreachable, and a bundler drops it. Use the flag to cut the file on disk,
not to cut what ships.
To get the compiled module without changing code written against the runtime API, use
compileAway in @ata-project/unplugin: a
new Validator(schema) whose schema is known at build time is replaced with the compiled
module wrapped by fromCompiled() from ata-validator/compiled, which answers validate(),
isValidObject(), validateJSON() and isValidJSON() as the runtime does, defaults and errors
included. For the plugin's three-schema test entry a minified Vite build goes from 122.5 KB to
16.7 KB gzipped on ata 1.39.2. In Node, loading the wrapper and a compiled module and answering the first two
checks takes 1.12 ms where the runtime takes 6.63 ms (median of 15 fresh processes). Across
SchemaStore's 977 schemas, 725 can be compiled away; the rest stay on the runtime. Where the code only calls isValidObject() or isValidJSON(), the plugin uses
fromCompiledVerdict() from ata-validator/compiled-verdict instead, which carries no error pipeline: a
small app that only asks for a boolean bundles to 2.6 KB gzipped, against 12.1 KB with the full wrapper, on
ata 1.40.0 with @ata-project/unplugin 0.5.0.
Programmatic API if you prefer to script it:
const fs = require('fs');
const { toStandaloneModule } = require('ata-validator/build');
fs.writeFileSync('./user.validator.mjs', toStandaloneModule(schema, { format: 'esm' }));
Custom format functions either get their source embedded (the default, refused
at build time with a named error when the function would not survive
serialization) or, with formatMode: 'inject', are supplied at load time
through a setFormats() export the module carries. docs/API.md has the
details.
Fastify startup, 10 route schemas, from a cold process to the first validated request:
ajv 19.7 ms, ata 2.75 ms, no build step required. ata registers in 0.25 ms of that and
compiles on the first request, so counting only registration would overstate the gap.
Reproduce with node benchmark/bench_fastify_boot.mjs.
Standard Schema V1
const v = new Validator(schema);
const result = v['~standard'].validate(data);
Fastify Plugin
Measured against Fastify's own schema and validation test files with ata swapped in as the default validator: 178 of 184 tests pass, and the remaining six assert the default validator's own extension API rather than validation behavior. Validation errors follow the schema's keyword declaration order, so error-message plugins and transformErrors hooks written against the default validator see the same order.
npm install fastify-ata
const fastify = require('fastify')();
fastify.register(require('fastify-ata'), {
coerceTypes: true,
removeAdditional: true,
});
C++
#include "ata.h"
auto schema = ata::compile(R"({
"type": "object",
"properties": { "name": {"type": "string"} },
"required": ["name"]
})");
auto result = ata::validate(schema, R"({"name": "Mert"})");
Custom keywords
Register keywords the schema vocabulary does not have with the keywords option. A definition is a validate function, a compile factory, or a macro that returns a schema, with an optional type that limits which values it sees.
const v = new Validator(
{ properties: { title: { type: 'string', maxWords: 5 } } },
{
keywords: {
maxWords: {
type: 'string',
compile: (n) => (s) => s.split(/\s+/).length <= n,
},
},
},
)
v.validate({ title: 'one two three four five six' })
A schema that uses a custom keyword runs on the interpreted engine, where anyOf, not and $ref keep their meaning around the custom check, and it cannot be compiled ahead of time, since a standalone module imports nothing and cannot carry the function. bundleStandalone refuses such schemas rather than emitting a module that would ignore the keyword. See docs/custom-keywords.md.
Framework integrations
Copy-paste recipes for the common frameworks. Most need 10-20 lines of glue. See docs/integrations for the full set.
Supported 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, $anchor, $dynamicRef, $dynamicAnchor |
| Boolean | true, false |
| v1 | propertyDependencies |
Dialects
$schema selects the dialect. Draft 2020-12 is the default, draft-07 is normalized on the way in, and https://json-schema.org/v1 (or the dated https://json-schema.org/v1/2026) selects JSON Schema v1.
Two things differ under v1. propertyDependencies selects a subschema by the value of a property rather than by its presence, which is what dependentSchemas does. And $dynamicRef no longer requires bookending: the reference resolves through the dynamic scope whether or not the schema it first lands on carries a matching $dynamicAnchor, so the outermost matching anchor still in scope wins. Everything else ata implements is identical under both dialects, so a schema that declares no $schema behaves exactly as before.
Both are implemented in the interpreted engine, so a v1 schema that uses $dynamicRef validates there rather than through the compiler or the native addon, which resolve the 2020-12 way. Schemas that use neither keyword take the same compiled path they always did.
Format Validators (hand-written, no regex)
email, date, date-time, time, uri, uri-reference, ipv4, ipv6, uuid, hostname
Known limitations
Running the whole Draft 2020-12 suite with nothing excluded, format and default under specification semantics (assertFormat: false, useDefaults: false), gives 1301 of 1301 cases. Draft 7 gives 929 of 929. The v1 dialect gives 1135 of 1135. npm run test:suite reproduces all three.
Areas that remain deliberate scope decisions for 1.x:
- Remote
$ref over the network is not fetched. Register cross-schema refs explicitly with schemas: [...] or addSchema().
$vocabulary is honoured for the document which names the meta-schema: a keyword whose vocabulary that meta-schema does not declare is not part of the dialect, so it is treated as unknown and does not apply. Two things it does not do. It does not refuse a schema whose meta-schema requires a vocabulary ata does not recognise, which the specification says an implementation must do; that schema is evaluated with every keyword applied, as before. And a separate document reached through $ref keeps its own keywords rather than inheriting the referring dialect, so register it with its own $schema if it should follow one.
contentEncoding / contentMediaType / contentSchema are annotation-only, as the spec permits, and are not validated.
minLength/maxLength count UTF-16 code units, not grapheme clusters.
- Infinite-loop detection relies on a recursion depth guard that cuts off circular
$ref chains.
default values are applied to validated data by default, where the spec treats default as a non-enforcing annotation. Pass useDefaults: false for the specification behavior.
format is asserted by default rather than treated as an annotation. Pass assertFormat: false for the specification behavior.
If one of these blocks you, open an issue; scope decisions get revisited with real use cases.
Building from Source
This section applies to contributors building the repository. Regular npm install users need not have a C++ toolchain.
Development prerequisites
Native builds require C/C++ toolchain support and the following libraries:
Install them before running npm run build:
brew install re2 abseil mimalloc
sudo apt-get update
sudo apt-get install -y libre2-dev libabsl-dev libmimalloc-dev
cmake -B build
cmake --build build
./build/ata_tests
npm install
npm run build
npm test
npm run test:suite
Project
- CHANGELOG.md records every release. It is kept in the
repository and not shipped in the npm package, where its history had grown to
about 13% of the install.
- CONTRIBUTING.md explains how to build the project and what a
pull request needs before it can be merged.
- GOVERNANCE.md says who decides what, which changes the project
will not accept and why, how releases are cut, and how someone becomes a
maintainer. It states the bus factor plainly rather than leaving you to find
it out.
- SECURITY.md is where vulnerabilities go. Not the issue tracker.
- CODE_OF_CONDUCT.md applies everywhere the project is
discussed.
License
MIT
Contributors
@mertcanaltin ·
@SukeshP1995 ·
@lemire ·
@armagandalkiran ·
@pnodet
Missing from this list after landing a patch? Send a pull request.