ata-validator 1.48.0 → 1.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -43,10 +43,12 @@ npm install ata-validator --omit=optional
43
43
 
44
44
  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.
45
45
 
46
- Where the size of what ships matters more than nanoseconds per call, `ata-validator/lite` is
47
- the same `Validator` on the interpreted engine alone, without the code generator: the same
48
- verdicts and errors, 58 KB gzipped in a bundled worker against 136 KB for the full entry.
49
- It leaves out what needs the generator, `parse()` and the ahead-of-time bundle methods.
46
+ In Cloudflare Workers and Vercel Edge, where generated code cannot run, `ata-validator`
47
+ resolves to an entry without the code generator through the `workerd` and `edge-light` export
48
+ conditions: the same names and answers, and a Worker bundled by wrangler goes from 179.1 to
49
+ 70.4 KiB gzipped. On a page whose Content-Security-Policy blocks `unsafe-eval`, or in a
50
+ browser extension, no condition tells the bundler, so import `ata-validator/lite` there: the
51
+ same `Validator` on the interpreted engine alone.
50
52
 
51
53
  On Deno the same package is `npm:ata-validator` or `jsr:@ata/validator`; the JSR package
52
54
  re-exports the npm one at the same version, and CI runs the official suite under Deno. The
@@ -109,6 +111,29 @@ file.
109
111
  passes with nothing failed, errored or skipped under Bowtie's own runner. The v1 dialect is
110
112
  declared in a pending harness change that waits for a Bowtie release.
111
113
 
114
+ ### Side by side
115
+
116
+ [validator-benchmarks](https://github.com/ata-core/validator-benchmarks) is ata's own
117
+ comparison, so it comes after the ones above: eleven validators on three realistic bodies
118
+ and four scenes, each library in its own process, 27 samples per cell, every schema and
119
+ document in the repository. On an order body with formats and a pattern, 900 valid
120
+ documents, Node 24:
121
+
122
+ [![Documents a second on one order body, eleven validators](https://raw.githubusercontent.com/ata-core/validator-benchmarks/main/media/race-order-verdict-valid.gif)](https://github.com/ata-core/validator-benchmarks/blob/main/media/race-order-verdict-valid.mp4)
123
+
124
+ | | documents a second, the verdict | same, `new Function` refused |
125
+ |---|---:|---:|
126
+ | ata | 3,295,880 | 607,614 |
127
+ | ata, compiled module | 3,319,594 | 3,344,948 |
128
+ | typebox | 1,638,813 | EvalError |
129
+ | ajv | 1,285,492 | EvalError |
130
+ | typia | 939,572 | 964,507 |
131
+ | zod | 440,473 | 237,176 |
132
+
133
+ Where ata is behind is in the same tables: on a four-field body that fails, ajv answers the
134
+ verdict faster, and with every error read it does 6.6M a second against ata's 2.9M with rich
135
+ errors (7.6M with `richErrors: false`).
136
+
112
137
  ## Why AOT
113
138
 
114
139
  | Dimension | Schema | ata-AOT | runtime validator | Difference |
@@ -254,13 +279,13 @@ is the most common way to get a misleading number out of this library.
254
279
 
255
280
  | | compiled with `ata build` | runtime `new Validator(schema)` |
256
281
  |---|---|---|
257
- | In a bundle, gzipped | **2.1 KB** | 125.0 KB |
282
+ | In a bundle, gzipped | **2.1 KB** | 126.1 KB |
258
283
  | Time to a served request | **3.5 ms** | 7.6 ms |
259
284
  | Schema known when | build time | any time |
260
285
 
261
286
  The bundle row is the ten-field user schema in
262
287
  `tests/fixtures/error-dx/user.schema.json`, every export of the compiled module against
263
- `new Validator(schema)`, built with `bun build --minify --target=browser` on ata 1.47.0.
288
+ `new Validator(schema)`, built with `bun build --minify --target=browser` on ata 1.48.0.
264
289
  The startup row is a Hono route on Bun 1.4, the median of three rounds of best-of-seven, from
265
290
  `benchmark/bundle`, against 3.5 ms for the same app doing no validation at all, so the
266
291
  compiled path costs nothing measurable to start. The runtime figure is what it is because
@@ -551,6 +576,19 @@ npx ata build 'schemas/*.json' --out-dir build/validators --check
551
576
 
552
577
  Run with `--watch` during development for incremental rebuilds.
553
578
 
579
+ In a production build (`--no-source`, or `NODE_ENV=production`), `ata build` can leave the
580
+ error collector out of the modules: each one carries its verdict and its schema, and takes
581
+ its errors from one `ata-errors.mjs` the build writes beside them. The build does this only
582
+ where it makes the output smaller, so a single small schema keeps its self-contained module.
583
+ The errors are the ones `new Validator(schema, { richErrors: false })` reports, in the same
584
+ order. Seven SchemaStore schemas imported into one app, minified with esbuild, go from 148.9 KB
585
+ to 63.3 KB gzipped, and from 35.8 ms to 13.8 ms to import with code generation blocked. A
586
+ rejected document costs more this way, since its errors come from an interpreted walk:
587
+ `--errors inline` keeps every module self-contained. `--mode schema` writes each module as
588
+ its schema and takes the verdict from the runtime too: forty SchemaStore schemas in one app
589
+ come to 101.6 KB gzipped against 212.6 KB as compact modules, and load and check each once in
590
+ 23.0 ms against 35.5, while a check once warm costs about five times what generated code costs.
591
+
554
592
  Bundle sizes for the 10-field user schema in `tests/fixtures/error-dx/user.schema.json`,
555
593
  minified and gzipped, measured with `bun build --minify --target=browser` on ata 1.45.0:
556
594
 
@@ -703,7 +741,7 @@ Copy-paste recipes for the common frameworks. Most need 10-20 lines of glue. See
703
741
 
704
742
  ### Dialects
705
743
 
706
- `$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.
744
+ `$schema` selects the dialect. Draft 2020-12 is the default; draft-07, draft-06 and Draft 2019-09 are rewritten to the 2020-12 spelling on the way in (draft-06 as draft-07 without `if`/`then`/`else`, 2019-09's tuple `items` and `$recursiveRef` as `prefixItems` and `$dynamicRef`); and `https://json-schema.org/v1` (or the dated `https://json-schema.org/v1/2026`) selects JSON Schema v1. The official suite passes in full on all five: 1301 of 1301 on 2020-12, 1261 of 1261 on 2019-09, 929 of 929 on draft-07, 841 of 841 on draft-06 and 1135 of 1135 on v1, with code generation allowed and blocked. Draft-04 and earlier are not supported; draft-04's boolean `exclusiveMinimum`/`exclusiveMaximum` is read wherever it appears.
707
745
 
708
746
  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.
709
747
 
package/aot.d.ts CHANGED
@@ -24,6 +24,46 @@ export function toStandalone(validator: Validator<unknown>): string | null;
24
24
  /** Emit one standalone module (no runtime dependency) for one validator. */
25
25
  export function toStandaloneModule(validator: Validator<unknown>, opts?: StandaloneOptions): string;
26
26
 
27
+ /** Options of toCompactModule. */
28
+ export interface CompactModuleOptions {
29
+ /** Import specifier of the error runtime the module takes its errors from. */
30
+ errorRuntime: string;
31
+ /** Module format for the emitted source. Default: 'esm'. */
32
+ format?: 'esm' | 'cjs';
33
+ [key: string]: unknown;
34
+ }
35
+
36
+ /**
37
+ * Emit a compact module: the verdict function and the schema without its
38
+ * descriptive keywords, with error detail from the shared runtime at
39
+ * `opts.errorRuntime` (see errorRuntimeSource). The errors are the ones the
40
+ * runtime Validator reports with `richErrors: false`, in its order, and
41
+ * `errorMessage` is not applied, as in toStandaloneModule. Null when the validator depends on
42
+ * something outside its schema (custom keywords or formats, registered
43
+ * schemas, options other than the defaults); write the self-contained module
44
+ * then. `ata build` makes this choice by itself.
45
+ */
46
+ export function toCompactModule(validator: Validator<unknown>, opts: CompactModuleOptions): string | null;
47
+
48
+ /** Why a validator cannot be written as a compact module, or null when it can. */
49
+ export function compactDecline(validator: Validator<unknown>): string | null;
50
+
51
+ /**
52
+ * Emit a schema module: the schema without its descriptive keywords and a few
53
+ * lines; the verdict and the errors come from the runtime at `opts.runtime`
54
+ * (errorRuntimeSource with `schemaMode`). Far smaller and quicker to load than
55
+ * generated code, about five times slower per check once warm. Answers as the
56
+ * runtime Validator does with `richErrors: false`. Null where
57
+ * toCompactModule would be null. `ata build --mode schema` writes these.
58
+ */
59
+ export function toSchemaModule(validator: Validator<unknown>, opts: { runtime: string; format?: 'esm' | 'cjs' }): string | null;
60
+
61
+ /** Source of the runtime compact modules (and, with `schemaMode`, schema
62
+ * modules) import. With `schemas`, the validators or schemas whose modules
63
+ * will import it, parts none of them needs are left out. Node only: it is put
64
+ * together from the library's files. */
65
+ export function errorRuntimeSource(format?: 'esm' | 'cjs', schemas?: ReadonlyArray<unknown> | null, schemaMode?: boolean): string;
66
+
27
67
  /** Emit a module of per-schema compiled validators that `loadBundle` consumes. */
28
68
  export function bundle(V: ValidatorClass, schemas: ReadonlyArray<JSONSchema | boolean | object>, opts?: StandaloneOptions): string;
29
69
 
package/aot.mjs CHANGED
@@ -1,3 +1,3 @@
1
1
  import mod from './lib/aot-impl.js';
2
- export const { toStandalone, toStandaloneModule, bundle, bundleStandalone, bundleCompact, loadBundle } = mod;
2
+ export const { toStandalone, toStandaloneModule, toCompactModule, toSchemaModule, compactDecline, errorRuntimeSource, bundle, bundleStandalone, bundleCompact, loadBundle } = mod;
3
3
  export default mod;
package/bin/ata.js CHANGED
@@ -45,6 +45,12 @@ Build options:
45
45
  --abort-early Use stub errors (smallest bundle)
46
46
  --one-pass Report errors with the runtime's one-pass function: faster on
47
47
  rejected documents, a module 1.3 to 1.7x the size
48
+ --errors <mode> Where error detail comes from: auto | inline | shared. auto
49
+ (default) has modules share one error runtime file when that
50
+ makes the output smaller; production builds only (--no-source)
51
+ --mode <mode> code (default) | schema. schema writes each module as its
52
+ schema and takes the verdict and errors from one runtime
53
+ file: far smaller and quicker to load, slower per check
48
54
  --check Check (don't write); exit 1 if any output is stale
49
55
  --cache-file <path> Cache file for incremental builds (default: cache disabled)
50
56
  --max-size <bytes> Fail build if any compiled module exceeds this gzipped size
@@ -94,6 +100,8 @@ function parseArgs(argv) {
94
100
  if (a === '--no-types') { out.opts.types = false; continue; }
95
101
  if (a === '--abort-early') { out.opts.abortEarly = true; continue; }
96
102
  if (a === '--one-pass') { out.opts.onePass = true; continue; }
103
+ if (a === '--errors') { out.opts.errors = argv[++i]; continue; }
104
+ if (a === '--mode') { out.opts.mode = argv[++i]; continue; }
97
105
  if (a === '--check') { out.opts.check = true; continue; }
98
106
  if (a === '--strict') { out.opts.strict = true; continue; }
99
107
  if (a === '--strict-schema') { out.opts.strictSchema = true; continue; }
@@ -296,6 +304,8 @@ function cmdBuild(args) {
296
304
  suffix: args.opts.suffix,
297
305
  abortEarly: !!args.opts.abortEarly,
298
306
  onePass: args.opts.onePass === true ? true : undefined,
307
+ errors: args.opts.errors,
308
+ mode: args.opts.mode,
299
309
  check: !!args.opts.check,
300
310
  maxSize: args.opts.maxSize,
301
311
  strict: !!args.opts.strict,
@@ -312,7 +322,10 @@ function cmdBuild(args) {
312
322
  return;
313
323
  }
314
324
  for (const c of report.compiled) {
315
- process.stdout.write(`ata: ${c.input} -> ${c.output} (${c.bytes.toLocaleString()} bytes)\n`);
325
+ process.stdout.write(`ata: ${c.input} -> ${c.output} (${c.bytes.toLocaleString()} bytes${c.errors === 'compact' ? ', shared errors' : c.errors === 'schema' ? ', schema module' : ''})\n`);
326
+ }
327
+ if (report.runtime) {
328
+ process.stdout.write(`ata: error runtime -> ${report.runtime.output} (${report.runtime.bytes.toLocaleString()} bytes, shared by the modules above)\n`);
316
329
  }
317
330
  for (const c of report.cached) {
318
331
  process.stdout.write(`ata: cached ${c.input}\n`);
package/build.d.ts CHANGED
@@ -190,7 +190,7 @@ export function compiledSchemaFor(schema: unknown): object;
190
190
  * {@link compiledEligible} declines, one the emitter cannot compile, and one
191
191
  * whose detailed errors the generator cannot produce.
192
192
  */
193
- export function compiledModuleFor(schema: unknown, opts?: { format?: 'esm' | 'cjs' }): string | null;
193
+ export function compiledModuleFor(schema: unknown, opts?: { format?: 'esm' | 'cjs'; errorRuntime?: string }): string | null;
194
194
 
195
195
  /**
196
196
  * The Validator options `fromCompiled()` reproduces, so a plugin can tell
@@ -0,0 +1,24 @@
1
+ /** One error as a compact module reports it. */
2
+ export interface CompactError {
3
+ code?: string;
4
+ keyword: string;
5
+ instancePath: string;
6
+ schemaPath: string;
7
+ params: Record<string, unknown>;
8
+ message: string;
9
+ docUrl?: string;
10
+ [key: string]: unknown;
11
+ }
12
+
13
+ /**
14
+ * The error function a compact module calls for a document its verdict
15
+ * rejected: the errors the runtime Validator reports with
16
+ * `richErrors: false`, in schema order, or null when it finds none.
17
+ */
18
+ export function createErrors(schema: object, options?: { v1?: boolean }): (data: unknown) => CompactError[] | null;
19
+
20
+ /** The linear-time pattern engine compact modules share (the RegExp-like object it returns has `test`). */
21
+ export function compileSafe(source: string): { test(input: string): boolean };
22
+
23
+ /** The ata-validator version this runtime belongs to. */
24
+ export const ataVersion: string;
@@ -0,0 +1,40 @@
1
+ 'use strict';
2
+
3
+ // ata-validator/error-runtime: the error runtime compact modules import, for a
4
+ // bundler plugin that writes compact modules into the application's own
5
+ // bundle. The bundler then keeps one copy, shared with ata-validator/compiled
6
+ // where both are used. `ata build` writes the same runtime as a file instead
7
+ // (errorRuntimeSource in ata-validator/aot).
8
+ const ataVersion = require('./lib/version.js');
9
+
10
+ // The interpreter and the code table load on the first call, not at import:
11
+ // a compact module calls this only when a document it rejected is asked for
12
+ // its errors.
13
+ function createErrors(schema, options) {
14
+ const { createErrors: create } = require('./lib/error-runtime-entry.js');
15
+ const { codeOf } = require('./lib/error-code-of.js');
16
+ return create(schema, codeOf, options);
17
+ }
18
+
19
+ // The linear-time pattern engine, which a compact module imports from here
20
+ // instead of embedding its own copy.
21
+ let _safe = null;
22
+ function compileSafe(src) {
23
+ if (_safe === null) _safe = require('./lib/safe-regex.js').compileSafe;
24
+ return _safe(src);
25
+ }
26
+
27
+ // Warm-up, in a browser only: the runtime's code compiles on its first use,
28
+ // which a page would otherwise pay for on its first rejected document, about
29
+ // a millisecond. When the browser is idle, one small error list is built here
30
+ // instead, once, so the first real one finds the code compiled. Nothing it
31
+ // does reaches a caller.
32
+ const WARM_SCHEMA = { type: 'object', properties: { a: { type: 'string' }, b: { type: 'integer', minimum: 0 }, c: { anyOf: [{ type: 'string' }, { type: 'null' }] }, f: { type: 'string', format: 'email' } }, required: ['d'], additionalProperties: false };
33
+ function warm() { try { createErrors(WARM_SCHEMA, { flat: true })({ a: 1, b: -1, c: 2, f: 'x', e: 0 }); } catch (_) {} }
34
+ if (typeof requestIdleCallback === 'function') { try { requestIdleCallback(warm); } catch (_) {} }
35
+ // In Node the same happens once the event loop is first free after start-up,
36
+ // on a timer that does not keep the process alive: a script that exits at
37
+ // once never runs it, and a server has it done before its first request.
38
+ else if (typeof process === 'object' && process !== null && process.versions && process.versions.node && typeof setTimeout === 'function') { try { const t = setTimeout(warm, 0); if (t && typeof t.unref === 'function') t.unref(); } catch (_) {} }
39
+
40
+ module.exports = { createErrors, compileSafe, ataVersion };
@@ -0,0 +1,3 @@
1
+ import mod from './error-runtime.js';
2
+ export const { createErrors, compileSafe, ataVersion } = mod;
3
+ export default mod;
package/index.browser.mjs CHANGED
@@ -1,5 +1,14 @@
1
- // Browser ESM entry: the same code, with the native addon stubbed out by the
2
- // bundler through the package.json "browser" field.
3
- import mod from './index.js';
4
- export const { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, toTypeScript, defineSchema, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON } = mod;
5
- export default mod;
1
+ // Browser ESM entry, for bundlers: the validator core with the code generator
2
+ // registered, as index.js, but with each tool imported on its own (see
3
+ // lib/tools.mjs) so that the tools an application does not import stay out
4
+ // of its bundle. The native addon is stubbed by the package.json "browser"
5
+ // field.
6
+ import core from './lib/validator-core.js';
7
+ import engine from './lib/codegen-engine.js';
8
+ import { toTypeScript, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON } from './lib/tools.mjs';
9
+
10
+ core._registerCodegen(engine);
11
+
12
+ export const { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, defineSchema } = core;
13
+ export { toTypeScript, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON };
14
+ export default { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, toTypeScript, defineSchema, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON };
package/index.edge.js ADDED
@@ -0,0 +1,14 @@
1
+ 'use strict';
2
+
3
+ // The package for runtimes that cannot run generated code: Cloudflare Workers
4
+ // (the `workerd` export condition) and Vercel Edge (`edge-light`) refuse
5
+ // `new Function`, so the code generator in the full package could only ever
6
+ // fail there and fall back. This entry leaves it out and answers through the
7
+ // interpreted engine, as the full package does in those runtimes: the same
8
+ // names, the same verdicts and errors, a bundle about half the size. Modules
9
+ // from `ata build` and Validator.bundle() load as they do in a browser bundle
10
+ // (lib/aot.browser.js); the emitters are not here, as they are not there.
11
+ const core = require('./lib/validator-core.js');
12
+ core._registerAot(() => require('./lib/aot.browser.js'));
13
+
14
+ module.exports = require('./lib/public-api.js');
package/index.edge.mjs ADDED
@@ -0,0 +1,13 @@
1
+ // ESM edge entry (see index.edge.js): the core without the code generator,
2
+ // for Cloudflare Workers and Vercel Edge, with each tool imported on its own
3
+ // (lib/tools.mjs) so the ones an application does not import stay out of its
4
+ // bundle.
5
+ import core from './lib/validator-core.js';
6
+ import aotBrowser from './lib/aot.browser.js';
7
+ import { toTypeScript, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON } from './lib/tools.mjs';
8
+
9
+ core._registerAot(() => aotBrowser);
10
+
11
+ export const { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, defineSchema } = core;
12
+ export { toTypeScript, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON };
13
+ export default { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, toTypeScript, defineSchema, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON };