ata-validator 1.60.0 → 1.61.1

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.
Files changed (56) hide show
  1. package/README.md +134 -6
  2. package/bin/ata.js +84 -0
  3. package/build.d.ts +29 -0
  4. package/build.mjs +2 -0
  5. package/compiled-bare-rich.d.ts +6 -0
  6. package/compiled-bare.d.ts +7 -0
  7. package/compiled-bare.js +5 -0
  8. package/compiled-bare.mjs +35 -0
  9. package/compiled-object.d.ts +11 -0
  10. package/compiled-object.js +6 -0
  11. package/compiled-object.mjs +3 -0
  12. package/compiled-plain.d.ts +16 -0
  13. package/compiled-plain.js +5 -0
  14. package/compiled-plain.mjs +3 -0
  15. package/compiled.d.ts +3 -1
  16. package/index.browser.mjs +2 -2
  17. package/index.bundle.mjs +2 -2
  18. package/index.d.ts +9 -0
  19. package/index.edge.mjs +2 -2
  20. package/index.js +48 -0
  21. package/index.mjs +1 -1
  22. package/index.node.mjs +1 -1
  23. package/lib/aot-build.js +81 -6
  24. package/lib/aot-impl.js +24 -21
  25. package/lib/bun-target.js +612 -0
  26. package/lib/codegen-engine.js +9 -3
  27. package/lib/combined-runtime.js +1 -1
  28. package/lib/compiled-bare-rich.js +54 -0
  29. package/lib/compiled-bare.js +44 -0
  30. package/lib/compiled-common.js +17 -2
  31. package/lib/compiled-defaults.browser.js +9 -0
  32. package/lib/compiled-defaults.js +8 -0
  33. package/lib/compiled-object.js +123 -0
  34. package/lib/compiled-plain.js +57 -0
  35. package/lib/compiled.js +2 -1
  36. package/lib/email-literal.js +29 -0
  37. package/lib/enrich-error.js +5 -1
  38. package/lib/error-codes.js +6 -76
  39. package/lib/formats-source.js +20 -3
  40. package/lib/formats.js +1 -1
  41. package/lib/js-compiler-errors.js +49 -10
  42. package/lib/js-compiler.js +92 -13
  43. package/lib/json-rejections.js +159 -0
  44. package/lib/native-codes.js +67 -0
  45. package/lib/native-message.js +8 -0
  46. package/lib/native.browser.js +11 -0
  47. package/lib/public-api.js +1 -0
  48. package/lib/rejections.js +13 -152
  49. package/lib/standard-schema.js +1 -1
  50. package/lib/tier0.js +1 -1
  51. package/lib/validator-core.js +46 -4
  52. package/lib/verify.js +386 -0
  53. package/lib/version.js +1 -1
  54. package/lite.js +1 -0
  55. package/lite.mjs +1 -1
  56. package/package.json +44 -9
package/README.md CHANGED
@@ -12,11 +12,17 @@ JSON Schema validation that compiles for speed and still runs where code generat
12
12
 
13
13
  ## Who uses ata
14
14
 
15
- Projects whose code depends on ata, from GitHub's dependency graph, and the framework that lists
16
- its plugin, with what each does with it:
15
+ Projects whose code depends on ata, from GitHub's dependency graph, a framework that lists its
16
+ plugin, a form library that ships its resolver, and one company whose use is not public code,
17
+ with what each does with it:
17
18
 
19
+ - [Trendyol](https://github.com/Trendyol) validates configuration with ata in Puzzle, its
20
+ internal config tooling. The package is internal, so it is not in the public dependency
21
+ graph; this line rests on its maintainer.
18
22
  - [Socket](https://github.com/SocketDev) uses ata, compiled ahead of time, in the shared build
19
23
  tooling of its repositories.
24
+ - [React Hook Form](https://github.com/react-hook-form/resolvers) ships an ata resolver in
25
+ `@hookform/resolvers` (`@hookform/resolvers/ata-validator`), beside its other validators.
20
26
  - [Fastify](https://fastify.dev/ecosystem/) lists the
21
27
  [`fastify-ata`](https://github.com/ata-core/fastify-ata) plugin in its ecosystem.
22
28
  - [react-jsonschema-form](https://github.com/rjsf-team/react-jsonschema-form) ships an ata
@@ -48,7 +54,14 @@ resolves to an entry without the code generator through the `workerd` and `edge-
48
54
  conditions: the same names and answers, and a Worker bundled by wrangler goes from 179.1 to
49
55
  70.4 KiB gzipped.
50
56
 
51
- In a browser bundle the code generator is not part of the page. Every schema starts on the
57
+ In a browser, compile with the bundler plugin. [`@ata-project/unplugin`](https://github.com/ata-core/unplugin-ata)
58
+ turns each `new Validator(schema)` whose schema it can read at build time into a compiled
59
+ validator that gives the same answers, and the runtime stays out of the page. A form with two
60
+ validators, one reading errors and one asking only for a verdict, bundles to 10.7 KB gzipped,
61
+ or 3.1 KB with `richErrors: false` on the one that reads errors, against 53.2 KB for the same
62
+ code on the runtime (esbuild, minified, the runtime with code splitting).
63
+
64
+ Without the plugin, the code generator is still not part of the page. Every schema starts on the
52
65
  interpreted engine, which gives the same answers, and a validator called a thousand times within
53
66
  a second loads the generator in the background, through a dynamic import that Vite, webpack and
54
67
  Rollup put in a chunk of their own, and is rebuilt on generated code when the page is idle. A
@@ -68,7 +81,7 @@ details, and what Deno Deploy's refusal of dynamic code means for it, are in
68
81
 
69
82
  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.
70
83
 
71
- 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](docs/edge-runtimes.md).
84
+ 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](docs/edge-runtimes.md). Where generated code runs but slowly, as in an ahead-of-time compiled Bun executable, a module from `ata build` is plain JavaScript that imports nothing and calls no `new Function`: on Bun 1.4.0, import plus a first validation takes 0.66 ms with such a module against 10.70 ms with the runtime compiler (medians of 21 runs). [docs/proof-no-code-generation.md](docs/proof-no-code-generation.md) runs one schema in five of these places, Workers and a CSP page included, and shows the same answers from each.
72
85
 
73
86
  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:
74
87
 
@@ -643,7 +656,24 @@ checks takes 1.12 ms where the runtime takes 6.63 ms (median of 15 fresh process
643
656
  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
644
657
  `fromCompiledVerdict()` from `ata-validator/compiled-verdict` instead, which carries no error pipeline: a
645
658
  small app that only asks for a boolean bundles to 2.6 KB gzipped, against 12.1 KB with the full wrapper, on
646
- ata 1.40.0 with `@ata-project/unplugin` 0.5.0.
659
+ ata 1.40.0 with `@ata-project/unplugin` 0.5.0. Where it reads errors but never validates JSON text, the plugin
660
+ uses `fromCompiledObject()` from `ata-validator/compiled-object`, the full wrapper without
661
+ `validateJSON()` and `isValidJSON()` and the position map behind them; and the wrappers fill
662
+ defaults with the compiled module's own `applyDefaults`. A form app with one validator that reads
663
+ errors and one that asks for a boolean went from 16.7 to 13.9 KB gzipped (esbuild, minified),
664
+ ata 1.61.0 against master with `@ata-project/unplugin` 0.11.0 and the change. The wrappers on
665
+ their own: full 13.8 to 12.8 KB, without JSON entry points 10.9, verdict only 2.7 to 1.6.
666
+ A validator built with `{ richErrors: false }` is compiled away as well: the module writes its
667
+ errors in that plain shape (`compiledModuleFor(schema, { plainErrors: true })`) and
668
+ `fromCompiledPlain()` from `ata-validator/compiled-plain` hands them out as they are, with no
669
+ error pipeline in the bundle, 2.6 KB gzipped for the wrapper. Where the plugin sees a
670
+ validator only ever called (`v.validate(x)`, `v.isValidObject(x)`), never handed to other code,
671
+ it uses the bare wrappers of `ata-validator/compiled-bare` instead, which leave out Standard
672
+ Schema and the extension hook nothing can reach there: 0.3 KB gzipped for both, and
673
+ `ata-validator/compiled-bare-rich` for one that reads ata's own errors. The same form app
674
+ bundles to 10.7 KB gzipped, or 3.1 KB with `richErrors: false` on the validator that reads errors. The errors
675
+ then carry `keyword`, `instancePath`, `schemaPath`, `params` and `message`, as that option has
676
+ always given them, without `received`, `suggestion` or `docUrl`.
647
677
 
648
678
  Programmatic API if you prefer to script it:
649
679
 
@@ -667,6 +697,103 @@ first request, so counting only registration would overstate the gap. The first
667
697
  here comes in the same tick as setup; a server that listens first has the code generator
668
698
  loaded by then. Reproduce with `node benchmark/bench_fastify_boot.mjs`.
669
699
 
700
+ A server that listens first also gets the engine warmed: once the event loop is free, if a
701
+ validator was built and not yet called, ata runs one check of a small schema, accepted and
702
+ rejected, so that V8 compiles the generator's functions and the error builders then rather
703
+ than on the first request. Measured on a four-field schema with the generator already
704
+ loaded: the first verdict 3.1 to 3.4 ms without it, 0.5 to 0.6 with it; the first error
705
+ list 6.7 against 0.8; a second route's first verdict 0.9 to 1.0 against 0.7 to 0.9. It costs
706
+ the process 1.5 to 2 ms of CPU once. A script that builds a validator and checks in the
707
+ same tick never runs it, and the timer is unref'd, so a finished script does not stay for
708
+ it; `ATA_NO_WARM=1` turns it off.
709
+
710
+ ### Proving the build (`ata verify`)
711
+
712
+ A module written at build time is only a good idea if it answers as the
713
+ runtime it replaced. `ata verify` checks that on the project's own schemas:
714
+
715
+ ```bash
716
+ npx ata verify 'schemas/*.json' --samples test/fixtures
717
+ ```
718
+
719
+ For each schema it loads the module `ata build` wrote beside it, makes
720
+ documents from the schema (200 by default, half of them with one rule broken,
721
+ then a document on either side of each bound the schema states),
722
+ reads the project's sample documents (JSON files, JSON Lines, or a directory
723
+ of them), and has both the module and a `Validator` built from the schema now
724
+ answer every one. Verdicts and error lists have to agree; a module whose
725
+ schema hash no longer matches the schema is reported stale. The exit code is
726
+ non-zero on any disagreement, stale module or missing module, so the command
727
+ works as a CI step. This is the check that found the two silent divergences
728
+ fixed in 1.59.0 and 1.60.0, now in a form a project can run on every change.
729
+ It does not run the `.bun.ts` target, which needs Bun's compiler.
730
+
731
+ ### Bun, compiled ahead of time
732
+
733
+ Bun's ahead-of-time compiler (`bun build --compile --aot`, an open pull
734
+ request as of October 2026) runs code that a library hands to `new Function`
735
+ through an interpreter. Measured with its pull request build on one schema
736
+ and 1024 parsed documents, nanoseconds per document (`benchmark/proof/aot`):
737
+
738
+ | | JIT | ahead of time |
739
+ |---|---|---|
740
+ | a module from `ata build --target bun` | 25 | 24 |
741
+ | `new Validator(schema)`, generated code | 28 | 492 |
742
+ | `new Validator(schema)`, interpreted engine | 52 | 80 |
743
+
744
+ So, in order of preference:
745
+
746
+ 1. Compile the schema at build time. `@ata-project/unplugin/bun` does it in
747
+ `Bun.build`, including `Bun.build({ compile })`; `ata build` writes the
748
+ module by hand. The module is plain JavaScript and compiles with the
749
+ program: 24 ns, the same errors as the runtime.
750
+ 2. Where the runtime `Validator` stays, let it take the interpreted engine.
751
+ It does so by itself when Bun reports `Bun.isAOT`, when the executable is
752
+ built with `--disallow-code-generation-from-strings` (the program then
753
+ counts as closed for the compiler, too), or when `ATA_ENGINE=interpreter`
754
+ is set: 80 ns against 492 for the generated code there.
755
+ 3. Write the module for the compiler. `ata build schema.json --target bun`
756
+ writes it with a closed object's keys read once and compared with the
757
+ declared list in order (on a ten-item order with 1024 distinct documents,
758
+ 10% of them invalid, 105 to 107 ns under the compiler against 223 for the
759
+ form V8 gets; on V8 the module keeps for...in, on Bun's JIT a plain count,
760
+ each the cheaper form there), and answers a rejection with the one-pass
761
+ error function (189 ns with the error list on that order, against 724
762
+ with the older collector), the form every other build takes only with
763
+ `--one-pass` since it is 20% larger gzipped. Beside the module it writes
764
+ `schema.compiled.bun.ts`: the schema as a TypeScript type (kinds,
765
+ properties and which are required, items and fixed tuples, literals,
766
+ local definitions as named types), `validate` and `isValid` with that
767
+ type on them, and `parse(text)`, which declares the type where it parses
768
+ and has the module check the content. Every check is the module's, and
769
+ the file is a type for the program, not a faster path: on that order,
770
+ with the pull request build of 2026-10-25, `isValid` 118 ns through the
771
+ file against 118 to 121 through the module, `parse` 564 to 574 against
772
+ 570 to 578 for a plain parse and the module, and reading the parsed
773
+ object after a typed parse 593 to 595 against 587 to 591 after a plain
774
+ one. The file is for executables built with
775
+ `--aot`; a project points that compiler at it with an `"aot"` condition
776
+ in its `package.json` `imports`, which nothing else reads:
777
+
778
+ ```json
779
+ { "imports": { "#schemas/*": { "aot": "./schemas/*.bun.ts", "default": "./schemas/*.js" } } }
780
+ ```
781
+
782
+ A schema no type can be written for (a remote `$ref`, `$dynamicRef`, a
783
+ root the type says nothing about) gets a warning and the plain module
784
+ alone.
785
+
786
+ The pull request carried its own typed validator until 2026-10-25, and
787
+ `--target bun` wrote the schema's rules as a type for it; it left the pull
788
+ request to return as a change of its own. `docs/API.md` has what ata keeps
789
+ for it. A schema's `pattern` is a `new RegExp` at run time in the runtime
790
+ `Validator`, which that compiler runs through its regular expression
791
+ interpreter; the module writes each pattern as a literal, which it compiles.
792
+ `--aot` makes TypeScript errors fatal; ata's declarations say `message?:
793
+ string` on an error, so read it as such. The numbers are from the pull
794
+ request build of 2026-10-24 on one machine and will be run again when the
795
+ compiler ships.
796
+
670
797
  ### Standard Schema V1
671
798
 
672
799
  ```javascript
@@ -745,9 +872,10 @@ Copy-paste recipes for the common frameworks. Most need 10-20 lines of glue. See
745
872
  | Vite (build-time compile) | dedicated plugin | [`ata-vite`](https://github.com/ata-core/ata-vite), the Vite entry of the plugin above |
746
873
  | Next.js (build-time compile) | webpack entry of the plugin above | `@ata-project/unplugin/webpack` in `next.config.js` |
747
874
  | Hono | async middleware | [docs/integrations/hono.md](docs/integrations/hono.md) |
748
- | Elysia | direct handler check | [docs/integrations/elysia.md](docs/integrations/elysia.md) |
875
+ | Elysia | Standard Schema V1 in the `body` slot | [docs/integrations/elysia.md](docs/integrations/elysia.md) |
749
876
  | tRPC | Standard Schema V1 input | [docs/integrations/trpc.md](docs/integrations/trpc.md) |
750
877
  | TanStack Form | Standard Schema V1 validator | [docs/integrations/tanstack-form.md](docs/integrations/tanstack-form.md) |
878
+ | React Hook Form | resolver | [`@hookform/resolvers/ata-validator`](https://github.com/react-hook-form/resolvers/tree/master/ata-validator) |
751
879
  | Express | sync middleware | [docs/integrations/express.md](docs/integrations/express.md) |
752
880
  | Koa | async ctx middleware | [docs/integrations/koa.md](docs/integrations/koa.md) |
753
881
  | NestJS | validation pipe | [docs/integrations/nestjs.md](docs/integrations/nestjs.md) |
package/bin/ata.js CHANGED
@@ -13,6 +13,15 @@ Usage:
13
13
  ata build <glob>... [options] Compile a project's schemas (glob pattern) per file.
14
14
  ata validate <schema> <data> [options] Validate a JSON data file against a schema.
15
15
  ata migrate [dir] [options] Report what switching from ajv would change; --write applies it.
16
+ ata verify <glob>... [options] Check that each schema's compiled module answers as the runtime
17
+ does, on generated documents and the project's samples.
18
+
19
+ Verify options:
20
+ --generate <n> Documents made from each schema, half with one rule broken. Default: 200
21
+ --samples <path> A JSON, JSON Lines file or a directory of them, checked too (repeatable)
22
+ --seed <n> Seed for the generated documents. Default: 1
23
+ --json Report as JSON
24
+ --out-dir, --suffix, --format Where the modules are, as given to ata build
16
25
 
17
26
  Validate options:
18
27
  --pretty Render errors with source frames (default on TTY)
@@ -65,6 +74,9 @@ Build options:
65
74
  --cache-file <path> Cache file for incremental builds (default: cache disabled)
66
75
  --max-size <bytes> Fail build if any compiled module exceeds this gzipped size
67
76
  --strict Treat any AOT-incompatible schema as a build error (default: skip + warn)
77
+ --target bun Write every module for Bun's ahead-of-time compiler, and beside
78
+ each a .bun.ts with the schema as a TypeScript type and a typed
79
+ parse(text); the plain module answers every check
68
80
  --strict-schema Fail the build on schema authoring mistakes: unknown
69
81
  keywords (with a spelling suggestion), keywords the
70
82
  node's type makes inert, unresolvable local $refs,
@@ -117,6 +129,11 @@ function parseArgs(argv) {
117
129
  if (a === '--check') { out.opts.check = true; continue; }
118
130
  if (a === '--strict') { out.opts.strict = true; continue; }
119
131
  if (a === '--strict-schema') { out.opts.strictSchema = true; continue; }
132
+ if (a === '--target') { out.opts.target = argv[++i]; continue; }
133
+ if (a === '--generate') { out.opts.generate = Number(argv[++i]); continue; }
134
+ if (a === '--samples') { (out.opts.samples = out.opts.samples || []).push(argv[++i]); continue; }
135
+ if (a === '--seed') { out.opts.seed = Number(argv[++i]); continue; }
136
+ if (a === '--json') { out.opts.json = true; continue; }
120
137
  if (a === '--out-dir') { out.opts.outDir = argv[++i]; continue; }
121
138
  if (a === '--suffix') { out.opts.suffix = argv[++i]; continue; }
122
139
  if (a === '--cache-file') { out.opts.cacheFile = argv[++i]; continue; }
@@ -239,6 +256,15 @@ function cmdCompile(args) {
239
256
 
240
257
  const { Validator } = require('..');
241
258
  const aot = require('../lib/aot');
259
+ // `--target bun`: the module is written for JavaScriptCore, the engine the
260
+ // executable the target is for runs on.
261
+ if (args.opts.target === 'bun') {
262
+ require('../lib/js-compiler').setTargetEngine('jsc-aot');
263
+ // The executable's module answers rejections with the one-pass error
264
+ // function and carries no schema-source frames unless asked for.
265
+ if (args.opts.onePass === undefined) args.opts.onePass = true;
266
+ if (args.opts.source === undefined) args.opts.source = false;
267
+ }
242
268
  let v;
243
269
  try {
244
270
  v = new Validator(schema);
@@ -277,6 +303,25 @@ function cmdCompile(args) {
277
303
  const sizeBytes = Buffer.byteLength(src, 'utf8');
278
304
  process.stdout.write(`ata: compiled ${input} -> ${output} (${sizeBytes.toLocaleString()} bytes, ${format}${abortEarly ? ', abort-early' : ''})\n`);
279
305
 
306
+ if (args.opts.target !== undefined) {
307
+ if (args.opts.target !== 'bun') {
308
+ reportCompileError(input, `--target must be bun (got ${JSON.stringify(args.opts.target)})`);
309
+ process.exit(1);
310
+ }
311
+ const { writeBunTarget, Declined } = require('../lib/bun-target');
312
+ const tn = (args.opts.name || path.basename(input, path.extname(input)).replace(/[^A-Za-z0-9_]/g, '_').replace(/^([a-z])/, (m) => m.toUpperCase())) || 'Data';
313
+ try {
314
+ const w = writeBunTarget({ schema, input, output, typeName: tn });
315
+ process.stdout.write(`ata: bun target -> ${w.bunPath} (for bun build --compile --aot)\n`);
316
+ } catch (e) {
317
+ if (!(e instanceof Declined)) throw e;
318
+ // No type can be written; the plain module stands alone.
319
+ const msg = `--target bun: ${e.message}; the plain module stands alone`;
320
+ if (args.opts.strict) { reportCompileError(input, msg); process.exit(1); }
321
+ process.stderr.write(`ata: warning: ${input}: ${msg}\n`);
322
+ }
323
+ }
324
+
280
325
  // Emit paired declaration file unless --no-types.
281
326
  // TypeScript resolution: .mjs -> .d.mts, .cjs -> .d.cts, .js -> .d.ts.
282
327
  const emitTypes = args.opts.types !== false;
@@ -298,6 +343,10 @@ function cmdCompile(args) {
298
343
  }
299
344
 
300
345
  function cmdBuild(args) {
346
+ if (args.opts.target !== undefined && args.opts.target !== 'bun') {
347
+ process.stderr.write(`error: --target must be bun (got ${JSON.stringify(args.opts.target)})\n`);
348
+ process.exit(1);
349
+ }
301
350
  if (args._.length === 0) {
302
351
  process.stderr.write('error: missing <glob>\n\n');
303
352
  usage();
@@ -323,6 +372,7 @@ function cmdBuild(args) {
323
372
  check: !!args.opts.check,
324
373
  maxSize: args.opts.maxSize,
325
374
  strict: !!args.opts.strict,
375
+ target: args.opts.target,
326
376
  types: args.opts.types,
327
377
  cacheFile: args.opts.cacheFile,
328
378
  // Forward the explicit user choice (true / false / undefined). The
@@ -337,6 +387,8 @@ function cmdBuild(args) {
337
387
  }
338
388
  for (const c of report.compiled) {
339
389
  process.stdout.write(`ata: ${c.input} -> ${c.output} (${c.bytes.toLocaleString()} bytes${c.errors === 'compact' ? ', shared errors' : c.errors === 'schema' ? ', schema module' : ''})\n`);
390
+ for (const t of (report.bunTargets || []).filter((t) => t.input === c.input)) process.stdout.write(`ata: bun target -> ${t.output} (for bun build --compile --aot)\n`);
391
+ for (const d of (report.bunDeclined || []).filter((d) => d.input === c.input)) process.stderr.write(`ata: warning: ${d.input}: ${d.reason}\n`);
340
392
  }
341
393
  if (report.runtime) {
342
394
  process.stdout.write(`ata: error runtime -> ${report.runtime.output} (${report.runtime.bytes.toLocaleString()} bytes, shared by the modules above)\n`);
@@ -408,6 +460,33 @@ function cmdBuild(args) {
408
460
  });
409
461
  }
410
462
 
463
+ function cmdVerify (args) {
464
+ if (args._.length === 0) {
465
+ process.stderr.write('error: missing <glob>\n\n');
466
+ usage();
467
+ process.exit(1);
468
+ }
469
+ const buildLib = require('../lib/aot-build');
470
+ const { verifyOne, renderReport, ok } = require('../lib/verify');
471
+ const format = args.opts.format || 'esm';
472
+ (async () => {
473
+ const inputs = await buildLib.expandGlobs(args._);
474
+ if (inputs.length === 0) {
475
+ process.stderr.write(`error: no schema files match ${args._.join(' ')}\n`);
476
+ process.exit(1);
477
+ }
478
+ const reports = [];
479
+ for (const input of inputs) {
480
+ let schema;
481
+ try { schema = buildLib.parseSchemaFile(input); } catch (e) { reports.push({ schema: input, module: '', error: e.message, documents: 0, agree: 0, disagreements: [] }); continue; }
482
+ const moduleFile = buildLib.outputPathFor(input, { format, outDir: args.opts.outDir, suffix: args.opts.suffix });
483
+ reports.push(await verifyOne({ schemaFile: input, moduleFile, schema, generate: Number.isFinite(args.opts.generate) ? args.opts.generate : 200, seed: Number.isFinite(args.opts.seed) ? args.opts.seed : 1, samples: args.opts.samples || [] }));
484
+ }
485
+ process.stdout.write(renderReport(reports, { json: !!args.opts.json }) + '\n');
486
+ process.exit(reports.every(ok) ? 0 : 1);
487
+ })().catch((e) => { process.stderr.write(`error: ${e.message}\n`); process.exit(1); });
488
+ }
489
+
411
490
  function cmdValidate (args) {
412
491
  if (args._.length < 2) {
413
492
  process.stderr.write('error: ata validate <schema> <data-file>\n');
@@ -508,6 +587,11 @@ function main() {
508
587
  return;
509
588
  }
510
589
 
590
+ if (cmd === 'verify') {
591
+ cmdVerify(args);
592
+ return;
593
+ }
594
+
511
595
  if (cmd === 'migrate') {
512
596
  const { migrate, formatReport } = require('../lib/migrate');
513
597
  const result = migrate(args._[0] || '.', { write: !!args.opts.write });
package/build.d.ts CHANGED
@@ -204,3 +204,32 @@ export function compiledModuleFor(schema: unknown, opts?: { format?: 'esm' | 'cj
204
204
  * ata-validator 1.37.0, where only calls without options can be.
205
205
  */
206
206
  export const compiledOptions: readonly string[];
207
+
208
+ /**
209
+ * The Bun target, for a build plugin: the schema as a TypeScript type and the
210
+ * `.bun.ts` module `ata build --target bun` writes beside a compiled module.
211
+ * With `tags: true` the type carries Bun's `Schema` tags and `residual` holds
212
+ * the schema of every rule the tagged type does not carry, the split made for
213
+ * Bun's `validate<T>`, which left the compiler's pull request on 2026-10-25
214
+ * and is to return as a change of its own; without tags `residual` is null.
215
+ */
216
+ export const bunTarget: {
217
+ schemaToBunType(schema: unknown, opts?: { name?: string; aliasPrefix?: string; tags?: boolean; assertFormat?: boolean }): { name: string; type: string; aliases: Array<[string, string]>; residual: unknown | null; carried: string[] };
218
+ toBunModule(args: { schemaFile: string; moduleImport: string; typeName: string; typeSource: string; aliases: Array<[string, string]> }): string;
219
+ writeBunTarget(args: { schema: unknown; input: string; output: string; typeName: string }): { bunPath: string; definitions: number; carried: number };
220
+ Declined: new (reason: string, pointer: string) => Error & { reason: string; pointer: string };
221
+ };
222
+
223
+ /** `ata verify` as functions: a compiled module held against the runtime on generated documents and samples. */
224
+ export const verify: {
225
+ verifyOne(args: { schemaFile: string; moduleFile: string; schema: unknown; generate?: number; seed?: number; samples?: string[] }): Promise<VerifyReport>;
226
+ verifySource(schema: unknown, src: string, opts?: { name?: string; format?: 'esm' | 'cjs'; generate?: number; seed?: number; samples?: string[] }): Promise<VerifyReport>;
227
+ generate(schema: unknown, count: number, seed: number): unknown[];
228
+ renderReport(reports: VerifyReport[], opts?: { json?: boolean }): string;
229
+ ok(report: VerifyReport): boolean;
230
+ };
231
+ export interface VerifyReport {
232
+ schema: string; module: string; documents: number; generated: number; samples: number; valid: number; agree: number;
233
+ disagreements: Array<{ source: string; document: string; runtime: string; module: string; runtimeIsValid: boolean; moduleIsValid: boolean } | { more: number }>;
234
+ stale: boolean; missing: boolean; ataVersion: string | null; error: string | null;
235
+ }
package/build.mjs CHANGED
@@ -12,5 +12,7 @@ export const schemaHash = mod.schemaHash;
12
12
  export const compiledEligible = mod.compiledEligible;
13
13
  export const compiledSchemaFor = mod.compiledSchemaFor;
14
14
  export const compiledModuleFor = mod.compiledModuleFor;
15
+ export const bunTarget = mod.bunTarget;
16
+ export const verify = mod.verify;
15
17
  export const compiledOptions = mod.compiledOptions;
16
18
  export default mod;
@@ -0,0 +1,6 @@
1
+ // ata-validator/compiled-bare-rich: the compiled-bare wrapper for a validator
2
+ // a bundler plugin sees only called, that reads ata's own errors.
3
+ import type { ValidationResult } from './index.js';
4
+ import type { CompiledModule } from './compiled.js';
5
+
6
+ export function bareRich<T = unknown>(mod: CompiledModule, schema: object | boolean, useDefaults?: boolean): { validate(data: unknown): ValidationResult<T>; isValidObject(data: unknown): data is T };
@@ -0,0 +1,7 @@
1
+ // ata-validator/compiled-bare: the smallest compiled-module wrappers, for a
2
+ // validator a bundler plugin sees used only through direct method calls.
3
+ import type { ValidationResult } from './index.js';
4
+ import type { CompiledModule } from './compiled.js';
5
+
6
+ export function bareVerdict(mod: CompiledModule, useDefaults?: boolean): { isValidObject(data: unknown): boolean };
7
+ export function barePlain<T = unknown>(mod: CompiledModule, useDefaults?: boolean): { validate(data: unknown): ValidationResult<T>; isValidObject(data: unknown): data is T };
@@ -0,0 +1,5 @@
1
+ 'use strict';
2
+
3
+ // ata-validator/compiled-bare: the smallest compiled-module wrappers, for a
4
+ // bundler plugin. See lib/compiled-bare.js.
5
+ module.exports = require('./lib/compiled-bare.js');
@@ -0,0 +1,35 @@
1
+ // ata-validator/compiled-bare as an ES module of its own rather than a
2
+ // re-export of the CommonJS file: a bundler then has no CommonJS interop to
3
+ // add, which was more than half of the bundled size (662 to 315 bytes
4
+ // gzipped). The same functions as lib/compiled-bare.js;
5
+ // tests/test_compiled_bare.js holds the two to the same answers.
6
+
7
+ const EMPTY_ERRORS = Object.freeze([]);
8
+ const VERDICT_DISAGREES = Object.freeze({
9
+ valid: false,
10
+ errors: Object.freeze([Object.freeze({ keyword: 'validation', instancePath: '', schemaPath: '#', params: Object.freeze({}), message: 'schema validation failed' })]),
11
+ });
12
+
13
+ function verdictOf(mod, useDefaults) {
14
+ const fill = useDefaults !== false && typeof mod.applyDefaults === 'function' ? mod.applyDefaults : null;
15
+ return fill ? (d) => { fill(d); return mod.isValid(d); } : mod.isValid;
16
+ }
17
+
18
+ export function bareVerdict(mod, useDefaults) {
19
+ return { isValidObject: verdictOf(mod, useDefaults) };
20
+ }
21
+
22
+ export function barePlain(mod, useDefaults) {
23
+ const isValid = verdictOf(mod, useDefaults);
24
+ const errFn = mod.validate;
25
+ return {
26
+ validate(data) {
27
+ if (isValid(data)) return { valid: true, data, errors: EMPTY_ERRORS };
28
+ const r = errFn(data);
29
+ return r && r.valid === false ? { valid: false, errors: r.errors } : VERDICT_DISAGREES;
30
+ },
31
+ isValidObject: isValid,
32
+ };
33
+ }
34
+
35
+ export default { bareVerdict, barePlain };
@@ -0,0 +1,11 @@
1
+ // ata-validator/compiled-object: ata-validator/compiled without the JSON text
2
+ // entry points, for code that reads errors but never calls validateJSON().
3
+ import type { ValidationResult } from './index.js';
4
+ import type { CompiledModule, CompiledOptions } from './compiled.js';
5
+
6
+ export interface CompiledObjectValidator<T = unknown> {
7
+ validate(data: unknown): ValidationResult<T>;
8
+ isValidObject(data: unknown): data is T;
9
+ }
10
+
11
+ export function fromCompiledObject<T = unknown>(mod: CompiledModule, schema: object, options?: CompiledOptions): CompiledObjectValidator<T>;
@@ -0,0 +1,6 @@
1
+ 'use strict';
2
+
3
+ // ata-validator/compiled-object: ata-validator/compiled without validateJSON()
4
+ // and isValidJSON(), for code that reads errors but never validates JSON text.
5
+ // See lib/compiled-object.js.
6
+ module.exports = require('./lib/compiled-object.js');
@@ -0,0 +1,3 @@
1
+ import mod from './compiled-object.js';
2
+ export const { fromCompiledObject } = mod;
3
+ export default mod;
@@ -0,0 +1,16 @@
1
+ // ata-validator/compiled-plain: the compiled-module wrapper for a Validator
2
+ // built with `richErrors: false`, for a module written with `plainErrors: true`.
3
+ import type { ValidationResult } from './index.js';
4
+ import type { CompiledModule } from './compiled.js';
5
+
6
+ export interface CompiledPlainValidator<T = unknown> {
7
+ validate(data: unknown): ValidationResult<T>;
8
+ isValidObject(data: unknown): data is T;
9
+ }
10
+
11
+ export interface CompiledPlainOptions {
12
+ richErrors?: false;
13
+ useDefaults?: boolean;
14
+ }
15
+
16
+ export function fromCompiledPlain<T = unknown>(mod: CompiledModule, schema: object, options?: CompiledPlainOptions): CompiledPlainValidator<T>;
@@ -0,0 +1,5 @@
1
+ 'use strict';
2
+
3
+ // ata-validator/compiled-plain: the compiled-module wrapper for a Validator
4
+ // built with `richErrors: false`. See lib/compiled-plain.js.
5
+ module.exports = require('./lib/compiled-plain.js');
@@ -0,0 +1,3 @@
1
+ import mod from './compiled-plain.js';
2
+ export const { fromCompiledPlain } = mod;
3
+ export default mod;
package/compiled.d.ts CHANGED
@@ -3,7 +3,9 @@
3
3
  import type { ValidationResult } from './index.js';
4
4
 
5
5
  export interface CompiledModule {
6
- validate(data: unknown): { valid: boolean; errors: unknown[] };
6
+ // A module's accepted result is frozen, so the array is read-only here;
7
+ // under Bun's ahead-of-time compiler a type error fails the build.
8
+ validate(data: unknown): { valid: boolean; errors: readonly unknown[] };
7
9
  isValid(data: unknown): boolean;
8
10
  }
9
11
 
package/index.browser.mjs CHANGED
@@ -15,6 +15,6 @@ import { toTypeScript, renderPretty, renderCompact, toOutput, toRetryMessage, de
15
15
 
16
16
  core._registerTiering(tiering.createTiering(() => import('./lib/codegen-engine.js')));
17
17
 
18
- export const { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, defineSchema } = core;
18
+ export const { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, defineSchema, schemaOf } = core;
19
19
  export { toTypeScript, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON };
20
- export default { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, toTypeScript, defineSchema, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON };
20
+ export default { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, toTypeScript, defineSchema, schemaOf, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON };
package/index.bundle.mjs CHANGED
@@ -10,6 +10,6 @@ import { toTypeScript, renderPretty, renderCompact, toOutput, toRetryMessage, de
10
10
 
11
11
  core._registerCodegen(engine);
12
12
 
13
- export const { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, defineSchema } = core;
13
+ export const { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, defineSchema, schemaOf } = core;
14
14
  export { toTypeScript, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON };
15
- export default { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, toTypeScript, defineSchema, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON };
15
+ export default { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, toTypeScript, defineSchema, schemaOf, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON };
package/index.d.ts CHANGED
@@ -678,3 +678,12 @@ export function toTypeScript(schema: object, options?: { name?: string }): strin
678
678
  * Requires TypeScript >= 5.0 for the `const` type parameter.
679
679
  */
680
680
  export declare function defineSchema<const S extends JSONSchema>(schema: S): S;
681
+
682
+ /**
683
+ * The JSON Schema of a TypeScript type, written in at build time by
684
+ * `@ata-project/unplugin` from the type and the JSDoc tags on its properties
685
+ * (`@format email`, `@minimum 1`, `@pattern`, `@minLength`, `@maxItems`,
686
+ * `@asType integer`). `new Validator(schemaOf<User>())` then compiles like any
687
+ * static schema. Without the plugin the call throws: it has no run-time form.
688
+ */
689
+ export declare function schemaOf<T>(): JSONSchema;
package/index.edge.mjs CHANGED
@@ -8,6 +8,6 @@ import { toTypeScript, renderPretty, renderCompact, toOutput, toRetryMessage, de
8
8
 
9
9
  core._registerAot(() => aotBrowser);
10
10
 
11
- export const { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, defineSchema } = core;
11
+ export const { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, defineSchema, schemaOf } = core;
12
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 };
13
+ export default { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, toTypeScript, defineSchema, schemaOf, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON };
package/index.js CHANGED
@@ -20,6 +20,53 @@ core._setCodegenLoader(loadCodegen);
20
20
  const warm = setTimeout(loadCodegen, 0);
21
21
  if (typeof warm === 'object' && typeof warm.unref === 'function') warm.unref();
22
22
 
23
+ // A process whose first check comes after the event loop has turned, a
24
+ // server's first request, pays on that check for V8 compiling the generator's
25
+ // functions and the error builders: on a four-field schema 5 ms for the
26
+ // verdict and 7 to 10 ms for the first error list, measured with the
27
+ // generator already loaded. One check of a small schema, accepted and
28
+ // rejected, run once the loop is free, moves that off the request: 0.5 and
29
+ // 0.7 ms after it (the same warm-up as the browser build's, lib/tiering.js).
30
+ // It runs only where a validator was built and is still uncompiled when the
31
+ // loop turns: a script that builds one and checks in the same tick never runs
32
+ // it, and the timer is unref'd, so a finished script does not stay for it.
33
+ // ATA_NO_WARM=1 turns it off.
34
+ let built = null;
35
+ core._registerBuiltHook((v) => {
36
+ if (built === null) {
37
+ built = [];
38
+ const t = setTimeout(warmUp, 0);
39
+ if (typeof t === 'object' && typeof t.unref === 'function') t.unref();
40
+ }
41
+ if (built.length < 8) built.push(new WeakRef(v));
42
+ });
43
+ function warmUp () {
44
+ const list = built;
45
+ built = [];
46
+ core._registerBuiltHook(null);
47
+ if (process.env.ATA_NO_WARM === '1') return;
48
+ // Untouched: neither compiled for validate() nor a verdict or validate
49
+ // method read off it (the prototype's accessors bind the method on the
50
+ // instance at the first read, a verdict-only compile sets no other mark).
51
+ const untouched = (v) => !v._initialized && !Object.prototype.hasOwnProperty.call(v, 'isValidObject') && !Object.prototype.hasOwnProperty.call(v, 'validate');
52
+ if (!list.some((r) => { const v = r.deref(); return v !== undefined && untouched(v); })) return;
53
+ try {
54
+ const v = new core.Validator({
55
+ type: 'object',
56
+ properties: {
57
+ a: { type: 'string', minLength: 2, maxLength: 9, pattern: '^[a-z]+$', format: 'email' },
58
+ b: { type: 'integer', minimum: 1, maximum: 9 },
59
+ c: { type: 'array', items: { type: 'string' }, maxItems: 3 },
60
+ d: { const: true },
61
+ e: { type: 'object', properties: { f: { type: 'string' } }, required: ['f'] },
62
+ },
63
+ required: ['a', 'b'],
64
+ });
65
+ v.isValidObject({ a: 'ab@c.de', b: 2, c: ['x'], d: true, e: { f: 'x' } });
66
+ void v.validate({ a: '1', b: 0, c: [1], d: false, e: {} }).errors.length;
67
+ } catch { /* a warm-up never reaches the caller */ }
68
+ }
69
+
23
70
  // The public names. lib/public-api.js holds the same for index.edge.js; they
24
71
  // are written out here rather than required so that loading the package in
25
72
  // Node reads one file fewer (tests/test_edge_entry.js and
@@ -91,6 +138,7 @@ module.exports = {
91
138
  parseJSON: core.parseJSON,
92
139
  toTypeScript,
93
140
  defineSchema: core.defineSchema,
141
+ schemaOf: core.schemaOf,
94
142
  renderPretty,
95
143
  renderCompact,
96
144
  toOutput,
package/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
1
  import mod from './index.js';
2
- export const { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, toTypeScript, defineSchema, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON } = mod;
2
+ export const { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, toTypeScript, defineSchema, schemaOf, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON } = mod;
3
3
  export default mod;
package/index.node.mjs CHANGED
@@ -6,5 +6,5 @@
6
6
  // static import they can follow.
7
7
  import { createRequire } from 'node:module';
8
8
  const mod = createRequire(import.meta.url)('./index.js');
9
- export const { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, toTypeScript, defineSchema, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON } = mod;
9
+ export const { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, toTypeScript, defineSchema, schemaOf, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON } = mod;
10
10
  export default mod;