ata-validator 0.20.0 → 0.21.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/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  All notable changes to ata-validator are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/), and this project adheres to semantic versioning.
4
4
 
5
+ ## 0.20.1 - 2026-05-27
6
+
7
+ ### Fixed
8
+
9
+ - `JSONSchema.items` now accepts `boolean` so the typed `t.tuple([...])` output (which sets `items: false` to close the tail) type-checks against `JSONSchema` without a constraint error. `items: false` is valid JSON Schema and the runtime already honoured it; the type definition just had not been widened. Anyone consuming `t.tuple` from outside a project with `skipLibCheck` ran into a `TTuple incorrectly extends JSONSchema` error.
10
+
5
11
  ## 0.20.0 - 2026-05-27
6
12
 
7
13
  ### Added
package/README.md CHANGED
@@ -70,6 +70,27 @@ The `ata` CLI ships `ata validate <schema> <data>` for one-off checks. TTY auto-
70
70
 
71
71
  Errors carry a stable `code` field (`ATA####`), see the [error code registry](docs/error-codes.md). Each code has a permalink at `https://ata-validator.com/e/<CODE>`.
72
72
 
73
+ ### Custom messages
74
+
75
+ 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.
76
+
77
+ ```js
78
+ const v = new Validator({
79
+ type: 'object',
80
+ properties: {
81
+ age: { type: 'integer', minimum: 18, errorMessage: { minimum: 'must be 18 or older', type: 'age has to be a number' } },
82
+ email: { type: 'string', format: 'email', errorMessage: 'enter a valid email address' },
83
+ },
84
+ required: ['email'],
85
+ errorMessage: { required: { email: 'email is required' } },
86
+ })
87
+
88
+ v.validate({ age: 5 }).errors[0].message // 'must be 18 or older'
89
+ v.validate({}).errors[0].message // 'email is required'
90
+ ```
91
+
92
+ Schemas without an `errorMessage` keyword pay nothing: the override pass is only installed when one is present.
93
+
73
94
  ### Opting out
74
95
 
75
96
  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.
@@ -183,6 +204,52 @@ type Event = Infer<typeof event>
183
204
 
184
205
  `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.
185
206
 
207
+ #### Chainable authoring with `ata-validator/t`
208
+
209
+ 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:
210
+
211
+ ```ts
212
+ import { t } from 'ata-validator/t'
213
+ import { Validator, type Infer } from 'ata-validator'
214
+
215
+ const User = t.object({
216
+ id: t.integer(),
217
+ name: t.string({ minLength: 1 }),
218
+ email: t.optional(t.string({ format: 'email' })),
219
+ role: t.union([t.literal('admin'), t.literal('user')]),
220
+ })
221
+
222
+ type User = Infer<typeof User>
223
+ // { id: number; name: string; role: 'admin' | 'user'; email?: string }
224
+
225
+ const v = new Validator(User)
226
+ ```
227
+
228
+ The builder covers primitives (`string`, `number`, `integer`, `boolean`, `null`), composites (`object` with `optional` keys, `array`, `tuple`, `record`, `union`, `intersect`, `literal`, `const`, `enum`), and refs (`ref`). Optionality is carried by a Symbol marker that the emitted JSON Schema and ata's codegen never see, so the output is still a plain JSON Schema literal that you can pass to anything that takes one.
229
+
230
+ #### Async refinement
231
+
232
+ 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.
233
+
234
+ ```ts
235
+ import { t } from 'ata-validator/t'
236
+ import { validateAsync, parseAsync } from 'ata-validator'
237
+
238
+ const Signup = t.refine(
239
+ t.object({ username: t.string({ minLength: 3 }), email: t.string({ format: 'email' }) }),
240
+ async (value) => !(await usernameTaken(value.username)),
241
+ { message: 'username is already taken', path: '/username' },
242
+ )
243
+
244
+ const r = await validateAsync(Signup, body)
245
+ if (!r.valid) return reply.code(400).send(r.errors)
246
+
247
+ // or: resolves to the typed value, throws on failure with err.errors
248
+ const user = await parseAsync(Signup, body)
249
+ ```
250
+
251
+ 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.
252
+
186
253
  #### Composes with TypeBox, Zod, or your own types
187
254
 
188
255
  `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.
package/index.browser.mjs CHANGED
@@ -1,4 +1,4 @@
1
1
  // Browser ESM entry — same code, native addon stubbed out by bundler via "browser" field.
2
2
  import mod from './index.js';
3
- export const { Validator, validate, version, createPaddedBuffer, SIMDJSON_PADDING, renderPretty, renderCompact, renderJSON, toTypeScript } = mod;
3
+ export const { Validator, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, renderPretty, renderCompact, renderJSON, toTypeScript } = mod;
4
4
  export default mod;
package/index.d.ts CHANGED
@@ -75,6 +75,18 @@ export function renderJSON(errors: RichValidationError[], opts?: JSONRenderOptio
75
75
  /** A user-supplied format checker. Receives the candidate value, returns true if valid. */
76
76
  export type FormatChecker = (value: string) => boolean;
77
77
 
78
+ /**
79
+ * Per-keyword custom error messages for a subschema's `errorMessage`. Keys are
80
+ * keyword names (`type`, `minimum`, `pattern`, ...). `required` may be a single
81
+ * string or a map keyed by the missing property name. `_` is the fallback used
82
+ * when no keyword-specific entry matches.
83
+ */
84
+ export interface ErrorMessageMap {
85
+ required?: string | Record<string, string>;
86
+ _?: string;
87
+ [keyword: string]: string | Record<string, string> | undefined;
88
+ }
89
+
78
90
  /** The seven primitive `type` values defined by JSON Schema. */
79
91
  export type JSONSchemaTypeName =
80
92
  | 'string'
@@ -125,7 +137,7 @@ export interface JSONSchema {
125
137
  dependentSchemas?: Record<string, JSONSchema>;
126
138
 
127
139
  // array
128
- items?: JSONSchema | ReadonlyArray<JSONSchema>;
140
+ items?: JSONSchema | ReadonlyArray<JSONSchema> | boolean;
129
141
  prefixItems?: ReadonlyArray<JSONSchema>;
130
142
  additionalItems?: boolean | JSONSchema;
131
143
  contains?: JSONSchema;
@@ -160,6 +172,15 @@ export interface JSONSchema {
160
172
  /** OpenAPI compatibility: ata honors `nullable` as a JSON Schema extension. */
161
173
  nullable?: boolean;
162
174
 
175
+ /**
176
+ * Override the human-facing `message` on errors this subschema produces.
177
+ * A string replaces the message for any failing keyword; an object overrides
178
+ * per keyword, with `required` keyed by missing property name (or a single
179
+ * string) and `_` as a fallback. Other fields (`code`, `keyword`, `path`)
180
+ * are unaffected.
181
+ */
182
+ errorMessage?: string | ErrorMessageMap;
183
+
163
184
  /** Custom and vendor keywords are allowed without error. */
164
185
  [keyword: string]: unknown;
165
186
  }
@@ -444,6 +465,34 @@ export function validate<T = unknown>(
444
465
  data: unknown
445
466
  ): ValidationResult<T>;
446
467
 
468
+ /**
469
+ * Async validate for schemas built with `t.refine(...)`. Structural JSON Schema
470
+ * validation runs synchronously first; refinements are awaited only when the
471
+ * value is structurally valid. Failing refinements surface as errors with
472
+ * `keyword: 'refine'`. Accepts a schema literal or an existing {@link Validator}.
473
+ */
474
+ export function validateAsync<T>(
475
+ validator: Validator<T>,
476
+ data: unknown
477
+ ): Promise<ValidationResult<T>>;
478
+ export function validateAsync<const S extends JSONSchema>(
479
+ schema: S,
480
+ data: unknown
481
+ ): Promise<ValidationResult<Infer<S>>>;
482
+ export function validateAsync<T = unknown>(
483
+ schema: object,
484
+ data: unknown
485
+ ): Promise<ValidationResult<T>>;
486
+
487
+ /**
488
+ * Like {@link validateAsync}, but resolves to the validated data on success and
489
+ * rejects with an `Error` whose `errors` property holds the
490
+ * {@link ValidationError} list on failure.
491
+ */
492
+ export function parseAsync<T>(validator: Validator<T>, data: unknown): Promise<T>;
493
+ export function parseAsync<const S extends JSONSchema>(schema: S, data: unknown): Promise<Infer<S>>;
494
+ export function parseAsync<T = unknown>(schema: object, data: unknown): Promise<T>;
495
+
447
496
  /** Fast compile: returns a validate function directly. WeakMap cached, second call with same schema is near-zero cost. */
448
497
  export function compile<T = unknown>(
449
498
  schema: object | string,
package/index.js CHANGED
@@ -1064,6 +1064,44 @@ class Validator {
1064
1064
  };
1065
1065
  }
1066
1066
 
1067
+ // Custom error messages: if any subschema declares an `errorMessage`
1068
+ // keyword, install an outermost decorator that overrides the `message`
1069
+ // field of the errors it owns. Gated on a one-time scan so schemas without
1070
+ // errorMessage keep the validate hot path untouched. Layered after rich
1071
+ // enrichment so `code`/`keyword`/`path` are already final and only the
1072
+ // human-facing message changes.
1073
+ {
1074
+ const emLib = require('./lib/error-messages');
1075
+ const schemaStr = this._schemaStr || (this._schemaObj ? JSON.stringify(this._schemaObj) : '');
1076
+ if (emLib.schemaHasErrorMessages(schemaStr)) {
1077
+ const root = this._schemaObj;
1078
+ const wrap = (inner) => (arg) => {
1079
+ const result = inner(arg);
1080
+ if (result && result.valid === false && result.errors && result.errors.length && result !== ABORT_EARLY_RESULT) {
1081
+ const overridden = emLib.applyErrorMessages(result.errors, root);
1082
+ if (overridden !== result.errors) return { valid: false, errors: overridden };
1083
+ }
1084
+ return result;
1085
+ };
1086
+ if (this.validate) this.validate = wrap(this.validate);
1087
+ if (this.validateJSON) this.validateJSON = wrap(this.validateJSON);
1088
+ // validateAndParse routes through self.validate on the codegen path, but
1089
+ // the native-only path returns directly from the addon — wrap it so both
1090
+ // paths get overrides. The result shape carries `value`, preserved here.
1091
+ if (this.validateAndParse) {
1092
+ const innerVP = this.validateAndParse;
1093
+ this.validateAndParse = (arg) => {
1094
+ const result = innerVP(arg);
1095
+ if (result && result.valid === false && result.errors && result.errors.length) {
1096
+ const overridden = emLib.applyErrorMessages(result.errors, root);
1097
+ if (overridden !== result.errors) return { valid: false, value: result.value, errors: overridden };
1098
+ }
1099
+ return result;
1100
+ };
1101
+ }
1102
+ }
1103
+ }
1104
+
1067
1105
  // Save to identity cache for ultra-fast reuse with same schema object
1068
1106
  if (this._schemaObj && typeof this._schemaObj === 'object') {
1069
1107
  _identityCache.set(this._schemaObj, this);
@@ -1322,6 +1360,41 @@ function validate(schema, data) {
1322
1360
  return v.validate(data);
1323
1361
  }
1324
1362
 
1363
+ // Async validation for schemas built with `t.refine(...)`. Structural
1364
+ // validation runs synchronously first; refinements are awaited only when the
1365
+ // value is structurally valid (a refinement body may assume the right shape).
1366
+ // Accepts a schema literal or an existing Validator instance plus its schema.
1367
+ // Returns a Promise<ValidationResult>.
1368
+ async function validateAsync(schemaOrValidator, data) {
1369
+ const refineLib = require('./lib/refine');
1370
+ let validator, schema;
1371
+ if (schemaOrValidator instanceof Validator) {
1372
+ validator = schemaOrValidator;
1373
+ schema = validator._schemaObj;
1374
+ } else {
1375
+ schema = schemaOrValidator;
1376
+ validator = new Validator(schema);
1377
+ }
1378
+ const structural = validator.validate(data);
1379
+ if (!structural.valid) return structural;
1380
+ const refinements = refineLib.getRefinements(schema);
1381
+ if (!refinements) return structural;
1382
+ const issues = await refineLib.runRefinements(refinements, structural.data !== undefined ? structural.data : data);
1383
+ if (issues.length) return { valid: false, errors: issues };
1384
+ return structural;
1385
+ }
1386
+
1387
+ // parseAsync resolves to the validated data, or rejects with an Error whose
1388
+ // `.errors` carries the ValidationError list. Mirrors the parse/validate split
1389
+ // used by Zod-style callers.
1390
+ async function parseAsync(schemaOrValidator, data) {
1391
+ const result = await validateAsync(schemaOrValidator, data);
1392
+ if (result.valid) return result.data !== undefined ? result.data : data;
1393
+ const err = new Error('ata: async validation failed');
1394
+ err.errors = result.errors;
1395
+ throw err;
1396
+ }
1397
+
1325
1398
  function version() {
1326
1399
  if (native) return native.version();
1327
1400
  try { return require("./lib/version"); } catch { return "unknown"; }
@@ -1420,6 +1493,8 @@ module.exports = {
1420
1493
  Validator,
1421
1494
  compile,
1422
1495
  validate,
1496
+ validateAsync,
1497
+ parseAsync,
1423
1498
  version,
1424
1499
  createPaddedBuffer,
1425
1500
  SIMDJSON_PADDING,
package/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
1
  import mod from './index.js';
2
- export const { Validator, validate, version, createPaddedBuffer, SIMDJSON_PADDING, defineSchema, renderPretty, renderCompact, renderJSON } = mod;
2
+ export const { Validator, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, defineSchema, renderPretty, renderCompact, renderJSON } = mod;
3
3
  export default mod;
@@ -0,0 +1,88 @@
1
+ 'use strict';
2
+
3
+ // Custom error messages via the `errorMessage` keyword.
4
+ //
5
+ // A subschema may declare an `errorMessage` keyword to override the generated
6
+ // `message` on any error it owns:
7
+ //
8
+ // errorMessage: "single message for any failure of this subschema"
9
+ // errorMessage: {
10
+ // <keyword>: "msg", // e.g. minimum, type, pattern, format
11
+ // required: "msg" | { prop: "msg" },// keyed by missing property, or one string
12
+ // additionalProperties: "msg",
13
+ // _: "fallback msg", // used when no keyword-specific entry matches
14
+ // }
15
+ //
16
+ // Resolution: each error's owning subschema is located from its schemaPath (the
17
+ // keyword is the last pointer segment, the owner is everything before it). If the
18
+ // owner declares an errorMessage, the error's `message` is replaced. Errors are
19
+ // cloned, never mutated, so frozen/shared results stay intact. When no schema in
20
+ // the tree carries an errorMessage the decorator is never installed, so the
21
+ // validate hot path keeps its original cost.
22
+
23
+ // Locate the schema object that owns the failing keyword. Mirrors
24
+ // resolveSchemaByPath() in index.js: the last pointer segment is the keyword,
25
+ // so the owner is the node reached by walking every segment before it.
26
+ function resolveOwner (rootSchema, schemaPath) {
27
+ if (!schemaPath || typeof schemaPath !== 'string' || schemaPath[0] !== '#') return undefined;
28
+ const stripped = schemaPath.slice(1);
29
+ if (!stripped || stripped === '/') return rootSchema;
30
+ const parts = stripped.split('/').filter(Boolean).map((s) => s.replace(/~1/g, '/').replace(/~0/g, '~'));
31
+ let target = rootSchema;
32
+ for (let i = 0; i < parts.length - 1; i++) {
33
+ if (target == null || typeof target !== 'object') return undefined;
34
+ target = target[parts[i]];
35
+ }
36
+ return target;
37
+ }
38
+
39
+ // Pick the override string for a single error from its owner's errorMessage.
40
+ // Returns undefined when nothing matches, leaving the default message in place.
41
+ function pickMessage (em, err) {
42
+ if (em == null) return undefined;
43
+ if (typeof em === 'string') return em;
44
+ if (typeof em !== 'object') return undefined;
45
+
46
+ const kw = err.keyword;
47
+
48
+ if (kw === 'required') {
49
+ const r = em.required;
50
+ if (typeof r === 'string') return r;
51
+ if (r && typeof r === 'object') {
52
+ const prop = err.params && err.params.missingProperty;
53
+ if (prop != null && typeof r[prop] === 'string') return r[prop];
54
+ }
55
+ }
56
+
57
+ if (kw != null && typeof em[kw] === 'string') return em[kw];
58
+ if (typeof em._ === 'string') return em._;
59
+ return undefined;
60
+ }
61
+
62
+ // Returns true when the schema string is worth scanning at validate time.
63
+ function schemaHasErrorMessages (schemaStr) {
64
+ return typeof schemaStr === 'string' && schemaStr.indexOf('"errorMessage"') !== -1;
65
+ }
66
+
67
+ // Apply overrides across an error array. Returns the same array reference when
68
+ // nothing changed (so callers can cheaply detect a no-op), or a new array with
69
+ // cloned-and-overridden entries.
70
+ function applyErrorMessages (errors, rootSchema) {
71
+ if (!errors || !errors.length) return errors;
72
+ let changed = false;
73
+ const out = new Array(errors.length);
74
+ for (let i = 0; i < errors.length; i++) {
75
+ const err = errors[i];
76
+ out[i] = err;
77
+ if (!err || typeof err.schemaPath !== 'string') continue;
78
+ const owner = resolveOwner(rootSchema, err.schemaPath);
79
+ if (!owner || typeof owner !== 'object') continue;
80
+ const msg = pickMessage(owner.errorMessage, err);
81
+ if (msg == null) continue;
82
+ out[i] = Object.assign({}, err, { message: msg });
83
+ changed = true;
84
+ }
85
+ return changed ? out : errors;
86
+ }
87
+
88
+ module.exports = { schemaHasErrorMessages, applyErrorMessages, resolveOwner, pickMessage };
package/lib/refine.js ADDED
@@ -0,0 +1,68 @@
1
+ 'use strict';
2
+
3
+ // Async refinement support for the `/t` builder.
4
+ //
5
+ // JSON Schema is synchronous by design and cannot express a check that needs to
6
+ // await (uniqueness, a remote lookup, cross-field business rules). Those live on
7
+ // the builder via `t.refine(schema, check, opts)`. A refinement is carried on a
8
+ // Symbol key (like the OPTIONAL marker), so it is invisible to Object.keys,
9
+ // JSON.stringify, and ata's codegen: `new Validator(schema)` still performs
10
+ // plain structural validation and ignores refinements entirely.
11
+ //
12
+ // `validateAsync()` (in index.js) runs structural validation first and only
13
+ // awaits the refinements when the value is structurally valid — a refinement
14
+ // body may assume the value already has the right shape.
15
+
16
+ const REFINE = Symbol.for('ata.t.refine');
17
+
18
+ // Pull the refinement list off a schema node, or null when there is none.
19
+ function getRefinements (schema) {
20
+ if (!schema || typeof schema !== 'object') return null;
21
+ const r = schema[REFINE];
22
+ return Array.isArray(r) && r.length ? r : null;
23
+ }
24
+
25
+ // Attach a refinement to a schema, composing with any already present. Returns a
26
+ // new object; the input is not mutated. Enumerable Symbol keys (OPTIONAL, prior
27
+ // refinements) are copied by the spread, then the refinement list is replaced.
28
+ function attach (schema, check, opts) {
29
+ if (typeof check !== 'function') {
30
+ throw new TypeError('t.refine(schema, check, opts?) — check must be a function');
31
+ }
32
+ const prev = (schema && Array.isArray(schema[REFINE])) ? schema[REFINE] : [];
33
+ const o = opts || {};
34
+ const entry = { check, message: o.message, path: o.path || '' };
35
+ return Object.assign({}, schema, { [REFINE]: prev.concat(entry) });
36
+ }
37
+
38
+ // Build a single refinement issue in the validator's error shape.
39
+ function issue (entry, message) {
40
+ return {
41
+ keyword: 'refine',
42
+ instancePath: entry.path || '',
43
+ path: entry.path || '',
44
+ schemaPath: '',
45
+ params: {},
46
+ message: message != null ? message : (entry.message || 'value failed refinement'),
47
+ };
48
+ }
49
+
50
+ // Evaluate every refinement against `data`, awaiting async checks concurrently.
51
+ // A check that returns falsy — or throws — yields an issue. Returns the issues
52
+ // array (empty when all pass).
53
+ async function runRefinements (refinements, data) {
54
+ const issues = [];
55
+ await Promise.all(refinements.map(async (entry) => {
56
+ let passed;
57
+ try {
58
+ passed = await entry.check(data);
59
+ } catch (e) {
60
+ issues.push(issue(entry, entry.message || (e && e.message) || 'refinement threw'));
61
+ return;
62
+ }
63
+ if (!passed) issues.push(issue(entry));
64
+ }));
65
+ return issues;
66
+ }
67
+
68
+ module.exports = { REFINE, getRefinements, attach, runRefinements };
package/lib/t.js CHANGED
@@ -12,6 +12,7 @@
12
12
  // to compute `required` and strips it from the emitted property schema.
13
13
 
14
14
  const OPTIONAL = Symbol.for('ata.t.optional');
15
+ const { attach: attachRefine } = require('./refine');
15
16
 
16
17
  function string(opts) {
17
18
  return Object.assign({ type: 'string' }, opts);
@@ -116,6 +117,13 @@ function never() {
116
117
  return { not: {} };
117
118
  }
118
119
 
120
+ function refine(schema, check, opts) {
121
+ // Attach an async/sync refinement. The schema stays plain JSON Schema for
122
+ // structural validation; the refinement is carried on a Symbol key and only
123
+ // runs via validateAsync()/parseAsync(). Compose by wrapping again.
124
+ return attachRefine(schema, check, opts);
125
+ }
126
+
119
127
  module.exports = {
120
128
  string,
121
129
  number,
@@ -136,5 +144,6 @@ module.exports = {
136
144
  any,
137
145
  unknown,
138
146
  never,
147
+ refine,
139
148
  OPTIONAL,
140
149
  };
package/lib/version.js CHANGED
@@ -7,4 +7,4 @@
7
7
  //
8
8
  // Kept in lockstep with package.json by `tests/test_version_sync.js`.
9
9
 
10
- module.exports = '0.20.0';
10
+ module.exports = '0.21.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
4
4
  "description": "JSON Schema validation with first-class TypeScript and zero runtime cost. AOT compile to per-schema ESM modules with zero validator dependency. Generic Validator<T> for TypeBox/Zod/Valibot composition. Optional runtime API. Standard Schema V1 compatible.",
5
5
  "main": "index.js",
6
6
  "module": "index.mjs",
@@ -48,7 +48,7 @@
48
48
  "rebuild": "cmake-js rebuild --target ata",
49
49
  "prebuild": "pkg-prebuilds-copy --baseDir build/Release --source ata.node --name=ata --strip --napi_version=10",
50
50
  "prebuild-all": "npm run prebuild -- --arch x64 && npm run prebuild -- --arch arm64",
51
- "test": "node test.js && node tests/test_no_native.js && node tests/test_browser_nofs.js && node tests/test_browser_imports_guard.js && node tests/test_version_sync.js && node tests/test_safe_regex_source_sync.js && node tests/test_t_builder.js && node tests/test_safe_regex.js && node tests/test_safe_regex_integration.js && node tests/test_aot_build.js && node tests/test_aot_differential.js && node tests/test_aot_cli_build.js && node tests/test_aot_cli_smoke.js && node tests/test_bundle_standalone.js && node tests/test_standalone_anyof.js && node tests/test_standalone_formats.js && node tests/test_aot_additional_props_errors.js && node tests/test_id_anchor_refs.js && node tests/test_typed_validator_runner.js && node tests/test_define_schema.js && node tests/test_error_codes_lock.js && node tests/test_nullable.js && node tests/test_validate_and_parse.js && node tests/test_validate_data.js && node tests/test_enrich_error.js && node tests/test_rich_errors_optout.js && node tests/test_source_positions.js && node tests/fuzz_positions.js && node tests/test_data_positions.js && node tests/test_render_shared.js && node tests/test_renderers.js && node tests/test_runtime_error_dx.js && node tests/test_aot_error_dx.js && node tests/test_abort_early.js && node tests/test_branch_collapse.js && node tests/test_suggestions.js && node tests/test_cli_validate.js && node tests/test_cli_version.js && node benchmark/bench_aot_size.mjs",
51
+ "test": "node test.js && node tests/test_no_native.js && node tests/test_browser_nofs.js && node tests/test_browser_imports_guard.js && node tests/test_version_sync.js && node tests/test_safe_regex_source_sync.js && node tests/test_t_builder.js && node tests/test_async_refine.js && node tests/test_safe_regex.js && node tests/test_safe_regex_integration.js && node tests/test_aot_build.js && node tests/test_aot_differential.js && node tests/test_aot_cli_build.js && node tests/test_aot_cli_smoke.js && node tests/test_bundle_standalone.js && node tests/test_standalone_anyof.js && node tests/test_standalone_formats.js && node tests/test_aot_additional_props_errors.js && node tests/test_id_anchor_refs.js && node tests/test_typed_validator_runner.js && node tests/test_define_schema.js && node tests/test_error_codes_lock.js && node tests/test_nullable.js && node tests/test_validate_and_parse.js && node tests/test_validate_data.js && node tests/test_enrich_error.js && node tests/test_rich_errors_optout.js && node tests/test_error_messages.js && node tests/test_source_positions.js && node tests/fuzz_positions.js && node tests/test_data_positions.js && node tests/test_render_shared.js && node tests/test_renderers.js && node tests/test_runtime_error_dx.js && node tests/test_aot_error_dx.js && node tests/test_abort_early.js && node tests/test_branch_collapse.js && node tests/test_suggestions.js && node tests/test_cli_validate.js && node tests/test_cli_version.js && node benchmark/bench_aot_size.mjs",
52
52
  "bench:size": "node benchmark/bench_aot_size.mjs",
53
53
  "test:suite": "node tests/run_suite.js",
54
54
  "test:compat": "node tests/test_compat.js",
package/t.d.ts CHANGED
@@ -15,10 +15,21 @@
15
15
  //
16
16
  // const v = new Validator(User) // schema is plain JSON Schema
17
17
 
18
- import type { JSONSchema } from './index.js';
18
+ import type { JSONSchema, Infer } from './index.js';
19
19
 
20
20
  export declare const OPTIONAL: unique symbol;
21
21
 
22
+ /** Options for {@link TBuilder.refine}. */
23
+ export interface RefineOptions {
24
+ /** Message attached to the error when the refinement fails. */
25
+ message?: string;
26
+ /** instancePath the error points at (e.g. `/email`). Defaults to the root. */
27
+ path?: string;
28
+ }
29
+
30
+ /** A refinement check: receives the structurally-valid value, returns (a promise of) a boolean. */
31
+ export type RefineCheck<T> = (value: T) => boolean | Promise<boolean>;
32
+
22
33
  export type TOptional<S> = S & { readonly [OPTIONAL]: true };
23
34
 
24
35
  export type TSchema = JSONSchema;
@@ -181,6 +192,14 @@ export interface TBuilder {
181
192
  any(): TAny;
182
193
  unknown(): TAny;
183
194
  never(): TNever;
195
+
196
+ /**
197
+ * Attach an async (or sync) refinement to a schema. The schema stays plain
198
+ * JSON Schema for structural validation; the refinement runs only via
199
+ * `validateAsync()` / `parseAsync()`. Compose by wrapping again. The returned
200
+ * schema infers the same type, so `Infer<typeof refined>` is unchanged.
201
+ */
202
+ refine<S extends JSONSchema>(schema: S, check: RefineCheck<Infer<S>>, opts?: RefineOptions): S;
184
203
  }
185
204
 
186
205
  export declare const t: TBuilder;