ata-validator 1.34.0 → 1.36.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/index.js CHANGED
@@ -1,246 +1,538 @@
1
- // Native addon: optional. Core validate() uses JS codegen and works without it.
2
- // Buffer APIs (isValid, countValid, isValidParallel) require native.
3
- // Loading is delegated to lib/native-load.js so this file stays free of
4
- // platform probing and `path` (the browser entry must not pull those in).
5
- const native = require("./lib/native-load")();
6
- const { normalizeKeywords, schemaUsesKeywords } = require('./lib/keywords');
7
- const {
8
- compileToJS,
9
- compileToJSCodegen,
10
- compileToJSCodegenWithErrors,
11
- compileToJSCombined,
12
- } = require("./lib/js-compiler");
13
- const { normalizeDraft7, normalizeNullable, normalizeExclusiveBounds, stripFormatAssertions } = require("./lib/draft7");
14
- const { enabledKeywords, stripDisabledKeywords } = require("./lib/vocabularies");
15
- const { needsNormalization } = require("./lib/schema-scan");
16
- const { isV1Dialect } = require("./lib/dialect");
17
- const { classify } = require("./lib/shape-classifier");
18
- const { buildTier0Plan, tier0Validate } = require("./lib/tier0");
19
- const { createCache: _createPosCache } = require("./lib/data-position-cache");
20
-
21
- // Extract default values from a schema tree. Returns a function that applies
22
- // defaults to an object in-place (mutates), or null if no defaults exist.
23
- function buildDefaultsApplier(schema) {
24
- if (typeof schema !== "object" || schema === null) return null;
25
- const actions = [];
26
- collectDefaults(schema, actions);
27
- if (actions.length === 0) return null;
28
- return (data) => {
29
- for (let i = 0; i < actions.length; i++) actions[i](data);
30
- };
31
- }
1
+ 'use strict';
2
+
3
+ // The full package: the validator core with the code generator registered,
4
+ // plus the tools no Validator calls (TypeScript generation, renderers, output
5
+ // formats). The core lives in lib/validator-core.js so that
6
+ // ata-validator/lite can load it without either.
7
+ const core = require('./lib/validator-core.js');
8
+
9
+ const jsCompiler = require('./lib/js-compiler.js');
10
+ const { compileToJS, compileToJSCodegen } = jsCompiler;
11
+
12
+ // The compiled paths a Validator installs when the code generator produced a
13
+ // verdict function: the hybrid and combined forms of validate(), the lazy
14
+ // error resolvers, validateJSON() over simdjson and the scanner, abortEarly.
15
+ // They live here rather than in Validator#_ensureCompiled so that
16
+ // ata-validator/lite, which never has a verdict function, does not carry
17
+ // them. Called with the validator as `this` and the locals of
18
+ // _ensureCompiled in `ctx`; the error and combined functions are built later
19
+ // by the builders, so they are read through ctx.err() and ctx.combined().
20
+
21
+ // A rejection the native walker decided from the text alone. The errors come
22
+ // from parsing and validating in JS, on first read, so a caller that reads
23
+ // only `.valid` never parses the document.
24
+ class TextRejection {
25
+ constructor (text, validateText) {
26
+ this.valid = false;
27
+ this._text = text;
28
+ this._validateText = validateText;
29
+ this._errors = null;
30
+ }
32
31
 
33
- // Write an own property. Plain assignment of a key named `__proto__` does
34
- // not create a property at all: it hits the Object.prototype setter and
35
- // rewrites the object's prototype, which is how a schema could reach
36
- // Object.prototype itself. defineProperty has no such special case.
37
- function setOwn(obj, key, val) {
38
- if (key === "__proto__") {
39
- Object.defineProperty(obj, key, {
40
- value: val,
41
- writable: true,
42
- enumerable: true,
43
- configurable: true,
44
- });
45
- } else {
46
- obj[key] = val;
32
+ get errors () {
33
+ if (this._errors === null) this._errors = core._internals._mustReject(this._validateText(this._text)).errors;
34
+ return this._errors;
47
35
  }
48
36
  }
49
37
 
50
- function collectDefaults(schema, actions, path) {
51
- if (typeof schema !== "object" || schema === null) return;
52
- const props = schema.properties;
53
- if (!props) return;
54
- for (const [key, prop] of Object.entries(props)) {
55
- if (prop && typeof prop === "object" && prop.default !== undefined) {
56
- const defaultVal = prop.default;
57
- if (!path) {
58
- actions.push((data) => {
59
- if (typeof data === "object" && data !== null && !Object.hasOwn(data, key)) {
60
- setOwn(data,
61
- key,
62
- typeof defaultVal === "object" && defaultVal !== null
63
- ? JSON.parse(JSON.stringify(defaultVal))
64
- : defaultVal);
65
- }
66
- });
67
- } else {
68
- const parentPath = path;
69
- actions.push((data) => {
70
- let target = data;
71
- for (let j = 0; j < parentPath.length; j++) {
72
- if (typeof target !== "object" || target === null) return;
73
- // Own keys only. `target[key]` for an inherited name walks the
74
- // prototype chain: a parent named `__proto__` that the instance
75
- // does not carry resolved to Object.prototype, and the child
76
- // defaults were written onto it, for every object in the realm.
77
- if (!Object.hasOwn(target, parentPath[j])) return;
78
- target = target[parentPath[j]];
79
- }
80
- if (
81
- typeof target === "object" &&
82
- target !== null &&
83
- !Object.hasOwn(target, key)
84
- ) {
85
- setOwn(target,
86
- key,
87
- typeof defaultVal === "object" && defaultVal !== null
88
- ? JSON.parse(JSON.stringify(defaultVal))
89
- : defaultVal);
90
- }
91
- });
92
- }
38
+ function installCodegenPaths (ctx) {
39
+ const { ABORT_EARLY_RESULT, HYBRID_TIER_CALLS, SIMDJSON_THRESHOLD, VALID_RESULT, _bindVerdict, _jsonSyntaxRejection, _mustReject, getNative, isV1Dialect, resolveSchemaByPath } = core._internals;
40
+ const { jsFn, _isCodegen, preprocess, fusedRemove, options, schemaObj, useSimdjsonForLarge, _buildCombined, _buildErr } = ctx;
41
+ // errFn: the generated error function when it is safe, else the
42
+ // interpreted engine, on every platform alike.
43
+ const hasDynRef = this._schemaStr.includes('"$dynamicRef"') || this._schemaStr.includes('"$dynamicAnchor"')
44
+ // The interpreted engine re-validates failing data to produce full errors. If it disagrees with the codegen verdict
45
+ // (it should not), a generic error keeps the result consistent.
46
+ let _interp = null;
47
+ const jsOnlyFallback = (d) => {
48
+ if (jsFn(d)) return { valid: true, data: d, errors: [] };
49
+ if (!_interp) {
50
+ const { createInterpreter } = require('./lib/interpreter');
51
+ _interp = createInterpreter(schemaObj, {
52
+ schemaMap: this._schemaMap.size > 0 ? this._schemaMap : null,
53
+ formats: this._userFormats,
54
+ v1: isV1Dialect(schemaObj),
55
+ keywords: this._keywords,
56
+ });
93
57
  }
94
- // Recurse into nested object schemas
95
- if (prop && typeof prop === "object" && prop.properties) {
96
- collectDefaults(prop, actions, (path || []).concat(key));
58
+ const r = _interp.validate(d);
59
+ if (!r.valid) return r;
60
+ return {
61
+ valid: false,
62
+ errors: [{
63
+ keyword: 'validation',
64
+ instancePath: '',
65
+ schemaPath: '',
66
+ params: {},
67
+ message: 'schema validation failed'
68
+ }]
69
+ };
70
+ };
71
+ // The error generator declines unevaluated*; the interpreted engine
72
+ // reports those schemas correctly, so failing data is re-validated
73
+ // there. This used to be a placeholder error with no keyword and no
74
+ // path, which hid whatever had actually failed.
75
+ // Resolved on the first rejection rather than at compile time, because
76
+ // building the generator behind it is two thirds of what a first call
77
+ // costs and a caller that never reads an error never needs it. The probe
78
+ // moves here with it: it calls the generated function, so it cannot run
79
+ // before the function exists.
80
+ let _errOnlyImpl = null;
81
+ const errOnly = (d) => {
82
+ if (_errOnlyImpl === null) {
83
+ _buildErr();
84
+ let safe = null;
85
+ if (ctx.err()) {
86
+ try {
87
+ ctx.err()({}, true);
88
+ safe = (x) => ctx.err()(x, true);
89
+ } catch {}
90
+ }
91
+ // Where the generator declines, the interpreted engine answers. It used
92
+ // to be the native addon when one was installed, whose errors carry
93
+ // other wording and no schemaPath, so the same validator reported
94
+ // differently depending on what the platform had installed.
95
+ _errOnlyImpl = safe || jsOnlyFallback;
97
96
  }
98
- }
99
- }
100
-
101
- // Build a function that coerces property values to match schema types in-place.
102
- // Handles string→number, string→integer, string→boolean, number→string, boolean→string.
103
- function buildCoercer(schema) {
104
- if (typeof schema !== "object" || schema === null) return null;
105
- const actions = [];
106
- collectCoercions(schema, actions);
107
- if (actions.length === 0) return null;
108
- return (data) => {
109
- for (let i = 0; i < actions.length; i++) actions[i](data);
97
+ return _mustReject(_errOnlyImpl(d));
110
98
  };
111
- }
112
99
 
113
- function collectCoercions(schema, actions, path) {
114
- if (typeof schema !== "object" || schema === null) return;
115
- const props = schema.properties;
116
- if (!props) return;
117
- for (const [key, prop] of Object.entries(props)) {
118
- if (!prop || typeof prop !== "object" || !prop.type) continue;
119
- const targetType = Array.isArray(prop.type) ? null : prop.type;
120
- if (!targetType) continue;
100
+ // Best path: combined validator (single pass, validates + collects errors)
101
+ // Valid data: returns VALID_RESULT, no allocation
102
+ // Invalid data: collects errors in one pass (no double validation)
103
+ // Fallback: hybridFn or jsFn + errFn for schemas combined can't handle
104
+ // Test combined at compile time -- some schemas produce broken combined code
105
+ // Test combined at compile time -- some schemas (e.g. if/then/else)
106
+ // produce broken combined code that crashes on certain inputs.
107
+ // We probe with diverse data; if any throws, fall back to hybrid.
108
+ let _combinedProbed = false;
109
+ let _safeCombined = null;
110
+ const combinedIfSafe = () => {
111
+ if (_combinedProbed) return _safeCombined;
112
+ _combinedProbed = true;
113
+ _buildCombined();
114
+ if (ctx.combined()) {
115
+ try {
116
+ const probe = {};
117
+ // Populate probe with one key per known property to trigger nested paths
118
+ if (schemaObj && schemaObj.properties) {
119
+ for (const k of Object.keys(schemaObj.properties)) probe[k] = "";
120
+ }
121
+ if (schemaObj && schemaObj.if && schemaObj.if.properties) {
122
+ for (const k of Object.keys(schemaObj.if.properties)) probe[k] = "";
123
+ }
124
+ ctx.combined()(probe);
125
+ ctx.combined()({});
126
+ ctx.combined()(null);
127
+ ctx.combined()(0);
128
+ _safeCombined = ctx.combined();
129
+ } catch {}
130
+ }
131
+ return _safeCombined;
132
+ };
121
133
 
122
- const coerce = buildSingleCoercion(targetType);
123
- if (!coerce) continue;
134
+ // What the hybrid path hands to its error slot: the combined function
135
+ // when it is usable, since it validates and collects in one pass, and
136
+ // the error generator otherwise. Same order the eager code chose, just
137
+ // chosen on the first rejection.
138
+ let _errPreferredImpl = null;
139
+ const errPreferCombined = (d) => {
140
+ if (_errPreferredImpl === null) _errPreferredImpl = combinedIfSafe() || errOnly;
141
+ return _mustReject(_errPreferredImpl(d));
142
+ };
124
143
 
125
- if (!path) {
126
- actions.push((data) => {
127
- if (typeof data === "object" && data !== null && key in data) {
128
- const coerced = coerce(data[key]);
129
- if (coerced !== undefined) data[key] = coerced;
130
- }
144
+ // The boolean engine is the verdict authority for these paths; the
145
+ // final lazy wrapper uses it to skip error construction entirely.
146
+ if (!hasDynRef || _isCodegen) this._fastVerdict = preprocess ? null : jsFn;
147
+
148
+ if (options.abortEarly && jsFn && !hasDynRef) {
149
+ // abortEarly: do NOT enrich. Skip position lookups, suggestions, source maps.
150
+ // This is the perf-critical path for edge gateways. The richErrors wrap
151
+ // below recognises the ATA9000 stub keyword and passes the frozen result
152
+ // through unchanged, so a single shared object is returned per failure.
153
+ const _fn = jsFn;
154
+ this.validate = preprocess
155
+ ? (data) => { preprocess(data); return _fn(data) ? VALID_RESULT : ABORT_EARLY_RESULT; }
156
+ : (data) => (_fn(data) ? VALID_RESULT : ABORT_EARLY_RESULT);
157
+ } else if (hasDynRef && _isCodegen && jsFn) {
158
+ // $dynamicRef with JS codegen: direct path, no wrapper layers
159
+ const _fn = jsFn, _efn = errOnly, _R = VALID_RESULT;
160
+ this.validate = preprocess
161
+ ? (data) => { preprocess(data); return _fn(data) ? _R : _efn(data); }
162
+ : (data) => _fn(data) ? _R : _efn(data);
163
+ } else if (hasDynRef) {
164
+ // $dynamicRef without codegen: the interpreted engine. It scores the
165
+ // same on the suite's $dynamicRef cases as the native walker since the
166
+ // dynamic-scope fix, needs no addon, and gets the verdict-only mode.
167
+ if (!_interp) {
168
+ const { createInterpreter } = require('./lib/interpreter');
169
+ _interp = createInterpreter(schemaObj, {
170
+ schemaMap: this._schemaMap.size > 0 ? this._schemaMap : null,
171
+ formats: this._userFormats,
172
+ v1: isV1Dialect(schemaObj),
173
+ keywords: this._keywords,
131
174
  });
175
+ }
176
+ const interp = _interp;
177
+ this._fastVerdict = preprocess ? null : (d) => interp.isValid(d);
178
+ this.validate = preprocess
179
+ ? (data) => { preprocess(data); return interp.validate(data); }
180
+ : (data) => interp.validate(data);
181
+ } else if (jsFn && jsFn._hybridFactory) {
182
+ // Zero-wrapper: hybridFactory bakes VALID_RESULT + errFn into a single function
183
+ // No arrow function wrapper, no ternary, one function call
184
+ // The factory bakes the error function in as an argument and never
185
+ // calls it for a document that passes, so a resolver here costs the
186
+ // accepted path nothing and keeps the compile off the first call.
187
+ // Until the first rejection this is the hybrid: the verdict function,
188
+ // with the error resolver baked in and never called for a document that
189
+ // passes. That keeps the combined function's compile off the first call,
190
+ // which is two thirds of what a first call costs.
191
+ //
192
+ // From the first rejection on, the combined function answers directly.
193
+ // It decides and collects in one pass, so the verdict pass in front of it
194
+ // was validating the document a second time: 134.1 microseconds against
195
+ // its 45.3 on a 1000-user array. Swapping rather than starting there keeps
196
+ // the lazy compile, and swapping at all is only free because the combined
197
+ // function now costs what the verdict function costs on accepted
198
+ // documents (1.02x on that array, 0.99x on a small body) since
199
+ // additionalProperties stopped materialising its key array. The
200
+ // indirection this needs measured inside the noise at both sizes.
201
+ let impl = null;
202
+ const onReject = (data) => {
203
+ const combined = combinedIfSafe();
204
+ if (combined) { impl = combined; return _mustReject(combined(data)); }
205
+ return errOnly(data);
206
+ };
207
+ // Tiered: the first calls go through the verdict function and the
208
+ // resolver, and the hybrid is compiled once the validator is in use.
209
+ // Compiling it up front was a second parse of the whole schema on
210
+ // every validator, most of which answer a handful of requests or only
211
+ // verdicts. A refused compile stays on the first tier.
212
+ let warm = 0;
213
+ const first = (data) => {
214
+ if (++warm >= HYBRID_TIER_CALLS && impl === first) {
215
+ impl = jsFn._hybridFactory(VALID_RESULT, onReject) || ((d) => (jsFn(d) ? VALID_RESULT : onReject(d)));
216
+ return impl(data);
217
+ }
218
+ return jsFn(data) ? VALID_RESULT : onReject(data);
219
+ };
220
+ impl = first;
221
+ const run = (data) => impl(data);
222
+ this.validate = preprocess
223
+ ? (data) => { preprocess(data); return run(data); }
224
+ : run;
225
+ } else {
226
+ // No hybrid factory, so the assembly needs the function itself rather
227
+ // than a reference it can call later: build it now.
228
+ const safeCombinedFn = combinedIfSafe();
229
+ if (safeCombinedFn) {
230
+ this.validate = preprocess
231
+ ? (data) => { preprocess(data); return safeCombinedFn(data); }
232
+ : safeCombinedFn;
132
233
  } else {
133
- const parentPath = path;
134
- actions.push((data) => {
135
- let target = data;
136
- for (let j = 0; j < parentPath.length; j++) {
137
- if (typeof target !== "object" || target === null) return;
138
- target = target[parentPath[j]];
139
- }
140
- if (typeof target === "object" && target !== null && key in target) {
141
- const coerced = coerce(target[key]);
142
- if (coerced !== undefined) target[key] = coerced;
234
+ this.validate = preprocess
235
+ ? (data) => {
236
+ preprocess(data);
237
+ return jsFn(data) ? VALID_RESULT : errOnly(data);
238
+ }
239
+ : (data) => (jsFn(data) ? VALID_RESULT : errOnly(data));
240
+ }
241
+ }
242
+ // Verbose mode: populate parentSchema, schema and data on each error, the
243
+ // three fields the default error shape carries under the same option.
244
+ // `data` is the value the error points at; without it a caller has to
245
+ // walk the document by the instance path itself, which is what one
246
+ // migration ended up writing by hand. Errors may be frozen, so clone
247
+ // them with the extra fields.
248
+ if (this._verbose) {
249
+ const inner = this.validate;
250
+ const root = this._schemaObj;
251
+ const { resolvePointer } = require('./lib/pointer.js');
252
+ this.validate = (data) => {
253
+ const result = inner(data);
254
+ if (result && !result.valid && result.errors) {
255
+ const enriched = result.errors.map((err) => {
256
+ if (!err || err.parentSchema !== undefined) return err;
257
+ const parentSchema = resolveSchemaByPath(root, err.schemaPath);
258
+ // The last segment of the schema path is the keyword that
259
+ // failed, so its value on the parent is that keyword's schema.
260
+ const sp = typeof err.schemaPath === 'string' ? err.schemaPath : '';
261
+ const last = sp.slice(sp.lastIndexOf('/') + 1).replace(/~1/g, '/').replace(/~0/g, '~');
262
+ const keywordSchema = (parentSchema !== null && typeof parentSchema === 'object' && last)
263
+ ? parentSchema[last]
264
+ : undefined;
265
+ return {
266
+ ...err,
267
+ parentSchema,
268
+ schema: keywordSchema,
269
+ data: resolvePointer(data, err.instancePath, undefined),
270
+ };
271
+ });
272
+ return { valid: false, errors: enriched };
273
+ }
274
+ return result;
275
+ };
276
+ }
277
+ // The verdict methods answer validate()'s question without building the
278
+ // error list, so they run the same preprocess pass. Skipping it made the
279
+ // two disagree on input that coercion or a default would have fixed.
280
+ _bindVerdict(this, fusedRemove
281
+ ? (data) => fusedRemove(data) || (preprocess(data), jsFn(data))
282
+ : preprocess
283
+ ? (data) => { preprocess(data); return jsFn(data) }
284
+ : jsFn);
285
+ // Same preference as the object path: the combined function first, since
286
+ // it validates and collects in one pass, and the error generator behind
287
+ // it. `errPreferCombined` is that order, resolved on the first rejection
288
+ // instead of at compile time.
289
+ // Tiered the same way as validate() above.
290
+ let jsonHybrid = null;
291
+ let jsonWarm = 0;
292
+ const jsonValidateTiered = (obj) => {
293
+ if (jsonHybrid !== null) return jsonHybrid(obj);
294
+ if (++jsonWarm >= HYBRID_TIER_CALLS) {
295
+ jsonHybrid = (jsFn._hybridFactory && jsFn._hybridFactory(VALID_RESULT, errPreferCombined))
296
+ || ((o) => (jsFn(o) ? VALID_RESULT : errPreferCombined(o)));
297
+ return jsonHybrid(obj);
298
+ }
299
+ return jsFn(obj) ? VALID_RESULT : errPreferCombined(obj);
300
+ };
301
+ // Parsed text takes the same preprocess pass as a parsed object, so
302
+ // validate(obj) and validateJSON(text) answer the same for the same
303
+ // document. Without it, coercion, removal and defaults applied on one
304
+ // path and not the other.
305
+ // abortEarly holds for the text path too: the stub, not errors, as the
306
+ // scanner below and the interpreted engine already answered.
307
+ const jsonValidateInner = options.abortEarly
308
+ ? (obj) => (jsFn(obj) ? VALID_RESULT : ABORT_EARLY_RESULT)
309
+ : jsonValidateTiered;
310
+ const jsonValidateFn = preprocess
311
+ ? (obj) => { preprocess(obj); return jsonValidateInner(obj) }
312
+ : jsonValidateInner;
313
+ // A document at or above the simdjson threshold is answered by the native
314
+ // walker without being parsed, except for the shapes lib/buffer-gate.js
315
+ // lists, where the walker disagrees with validate(); those schemas never
316
+ // ask it. A rejection's errors come from the same path as a small
317
+ // document's, on first read. The addon's own validateJSON used to supply
318
+ // them and accepted documents validate() rejects, 231 of the official
319
+ // suite's cases once padded past the threshold.
320
+ let nativeText; // undefined until the first large document
321
+ const nativeVerdict = (jsonStr) => {
322
+ if (nativeText === undefined) {
323
+ nativeText = !!getNative() && !require('./lib/buffer-gate.js').bufferNeedsSlowPath(schemaObj, this._schemaMap, this._keywords);
324
+ if (nativeText) {
325
+ this._ensureNative();
326
+ if (!(this._fastSlot >= 0)) nativeText = false;
327
+ }
328
+ }
329
+ return nativeText ? getNative().rawFastValidate(this._fastSlot, Buffer.from(jsonStr)) : undefined;
330
+ };
331
+ const validateText = (jsonStr) => {
332
+ let obj;
333
+ try {
334
+ obj = JSON.parse(jsonStr);
335
+ } catch (e) {
336
+ if (!(e instanceof SyntaxError)) throw e;
337
+ return _jsonSyntaxRejection(e);
338
+ }
339
+ return jsonValidateFn(obj);
340
+ };
341
+ this.validateJSON = useSimdjsonForLarge && !preprocess
342
+ ? (jsonStr) => {
343
+ // `_skipNativeFast` is set by the scanner short-circuit below when it
344
+ // has already decided the document is invalid. The encode and the
345
+ // native call would run only to return false.
346
+ if (jsonStr.length >= SIMDJSON_THRESHOLD && this._skipNativeFast !== true) {
347
+ const ok = nativeVerdict(jsonStr);
348
+ if (ok === true) return VALID_RESULT;
349
+ if (ok === false) return options.abortEarly ? ABORT_EARLY_RESULT : new TextRejection(jsonStr, validateText);
143
350
  }
144
- });
351
+ return validateText(jsonStr);
352
+ }
353
+ : validateText;
354
+ // The addon validates the bytes as they are, which is the wrong answer
355
+ // when the schema asks for coercion, removal or defaults: those change
356
+ // what counts as valid. With a preprocess pass configured the text is
357
+ // parsed and run through the same path validate() takes.
358
+ const verdictFromText = (jsonStr) => {
359
+ let parsed;
360
+ try {
361
+ parsed = JSON.parse(jsonStr);
362
+ } catch (e) {
363
+ if (!(e instanceof SyntaxError)) throw e;
364
+ return false;
145
365
  }
366
+ if (preprocess) preprocess(parsed);
367
+ return jsFn(parsed);
368
+ };
369
+ this.isValidJSON = useSimdjsonForLarge && !preprocess
370
+ ? (jsonStr) => {
371
+ if (jsonStr.length >= SIMDJSON_THRESHOLD) {
372
+ const ok = nativeVerdict(jsonStr);
373
+ if (ok !== undefined) return ok;
374
+ }
375
+ return verdictFromText(jsonStr);
376
+ }
377
+ : verdictFromText;
378
+
379
+ // A schema-directed scanner answers the verdict from the JSON text
380
+ // without building the document. Parsing is around three quarters of the
381
+ // cost of a real request, and a caller that only wants yes or no should
382
+ // not pay it; a rejection can also stop at the byte that caused it
383
+ // instead of parsing the rest of a document that is already refused.
384
+ //
385
+ // It is wired only where the verdict IS the answer. On a path that has
386
+ // to produce errors, scanning an invalid document is work thrown away,
387
+ // so those keep parsing. `abortEarly` has no errors to produce, so it
388
+ // counts as a verdict path.
389
+ //
390
+ // Not wired when a preprocess pass is configured: coercion, removal and
391
+ // defaults rewrite the document before it is judged, and the scanner
392
+ // reads what arrived. The compiler declines any schema it cannot answer
393
+ // and a compiled scanner returns BAIL for a document shape it cannot
394
+ // answer, and then the parse path below takes over unchanged.
395
+ };
146
396
 
147
- // Recurse into nested object properties
148
- if (prop.properties) {
149
- collectCoercions(prop, actions, (path || []).concat(key));
150
- }
397
+ // The verdict function alone, for isValidObject() before a full compile: the
398
+ // error and combined generators are left for the first rejection.
399
+ installCodegenPaths.compileVerdict = function compileVerdict () {
400
+ const { compileCacheKey, _compileCache, _bindVerdict, _rememberInstance } = core._internals;
401
+ if (!this._schemaStr) this._schemaStr = JSON.stringify(this._schemaObj);
402
+ const sm = this._schemaMap.size > 0 ? this._schemaMap : null;
403
+ const mapKey = compileCacheKey(this._schemaStr, this._schemaMap);
404
+ // Custom formats are JS functions: skip the shared cache so different
405
+ // validators with the same schema string but different formats don't collide.
406
+ const cached = (this._userFormats || this._usesKeywords) ? null : _compileCache.get(mapKey);
407
+ if (cached && cached.jsFn) {
408
+ this._jsFn = cached.jsFn;
409
+ _bindVerdict(this, cached.jsFn);
410
+ _rememberInstance(this);
411
+ return;
151
412
  }
152
- }
153
-
154
- function buildSingleCoercion(targetType) {
155
- switch (targetType) {
156
- case "number":
157
- return (v) => {
158
- if (typeof v === "string") {
159
- const n = Number(v);
160
- if (v !== "" && !isNaN(n)) return n;
161
- }
162
- if (typeof v === "boolean") return v ? 1 : 0;
163
- };
164
- case "integer":
165
- return (v) => {
166
- if (typeof v === "string") {
167
- const n = Number(v);
168
- if (v !== "" && Number.isInteger(n)) return n;
169
- }
170
- if (typeof v === "boolean") return v ? 1 : 0;
171
- };
172
- case "string":
173
- return (v) => {
174
- if (typeof v === "number" || typeof v === "boolean") return String(v);
175
- };
176
- case "boolean":
177
- return (v) => {
178
- if (v === "true" || v === "1") return true;
179
- if (v === "false" || v === "0") return false;
180
- };
181
- default:
182
- return null;
413
+ const uf = this._userFormats;
414
+ const _cg = compileToJSCodegen(this._schemaObj, sm, uf);
415
+ const jsFn = _cg || compileToJS(this._schemaObj, null, sm);
416
+ this._jsFn = jsFn;
417
+ if (jsFn) {
418
+ _bindVerdict(this, jsFn);
419
+ _rememberInstance(this);
420
+ // A partial entry: the verdict function is real, the other two are not
421
+ // built yet rather than declined. `undefined` is the not-built marker
422
+ // the full compile's _buildErr/_buildCombined look for; `null` would read as
423
+ // "the compiler declined" and cost the schema its error function, which
424
+ // is the bug this cache had once already. `isCodegen` rides along so a
425
+ // validator that later reuses this entry reports the same engine it
426
+ // would have compiled to.
427
+ if (!uf) {
428
+ if (!cached) _compileCache.set(mapKey, { jsFn, combined: undefined, errFn: undefined, isCodegen: !!_cg, full: false });
429
+ else cached.jsFn = jsFn;
430
+ }
183
431
  }
184
- }
432
+ };
185
433
 
186
- // Build a function that removes properties not defined in schema.properties.
187
- // Walks nested objects recursively.
188
- function buildRemover(schema) {
189
- if (typeof schema !== "object" || schema === null) return null;
190
- const actions = [];
191
- collectRemovals(schema, actions);
192
- if (actions.length === 0) return null;
193
- return (data) => {
194
- for (let i = 0; i < actions.length; i++) actions[i](data);
434
+ // validateJSON and isValidJSON over a schema-directed scanner, which answers
435
+ // from the JSON text without building the document.
436
+ installCodegenPaths.installScanner = function installScanner (schemaObj, options) {
437
+ const { ABORT_EARLY_RESULT, VALID_RESULT, _bindEntry } = core._internals;
438
+ const self = this;
439
+ // Generating a scanner costs about 20 microseconds, measured, and it
440
+ // saves from around 85 nanoseconds on a small accepted document to
441
+ // several microseconds on a rejected one. Building it on the first
442
+ // call would therefore be a straight loss for a caller that checks one
443
+ // document and exits, so it is built once a caller has asked often
444
+ // enough that it is plainly doing this in a loop. A server passes the
445
+ // line during warm-up and never sees it.
446
+ const SCAN_AFTER = 64;
447
+ let calls = 0;
448
+ // undefined: not built. null: this schema has no scanner. Passing true
449
+ // builds it now, which is how the differential test reaches it.
450
+ this._ensureScanner = (now) => {
451
+ if (self._scanner === undefined) {
452
+ if (!now && ++calls < SCAN_AFTER) return undefined;
453
+ const built = require('./lib/scan-compiler').compileScanner(schemaObj, { userFormats: self._userFormats });
454
+ self._scanner = built ? built.scan : null;
455
+ }
456
+ return self._scanner;
195
457
  };
196
- }
197
-
198
- function collectRemovals(schema, actions, path) {
199
- if (typeof schema !== "object" || schema === null || !schema.properties)
200
- return;
201
-
202
- // If this level has additionalProperties: false, add a removal action
203
- if (schema.additionalProperties === false) {
204
- const allowed = new Set(Object.keys(schema.properties));
205
- if (!path) {
206
- actions.push((data) => {
207
- if (typeof data !== "object" || data === null || Array.isArray(data))
208
- return;
209
- const keys = Object.keys(data);
210
- for (let i = 0; i < keys.length; i++) {
211
- if (!allowed.has(keys[i])) delete data[keys[i]];
212
- }
213
- });
214
- } else {
215
- const parentPath = path;
216
- actions.push((data) => {
217
- let target = data;
218
- for (let j = 0; j < parentPath.length; j++) {
219
- if (typeof target !== "object" || target === null) return;
220
- target = target[parentPath[j]];
458
+ const byParsing = this.isValidJSON;
459
+ // The verdict is a pure function of the text, and the caller a
460
+ // gateway or a drift monitor keeps asking about is usually the same
461
+ // text: a config file re-read on a timer, a heartbeat body. One
462
+ // remembered (text, verdict) pair answers that case with a native
463
+ // string compare, which is a memcmp, instead of a scan. Withheld when
464
+ // user formats or custom keywords are present, since those are user
465
+ // functions and nothing guarantees they are pure.
466
+ const memoizable = !self._userFormats && !self._usesKeywords;
467
+ let _memoText = null;
468
+ let _memoVerdict = false;
469
+ this.isValidJSON = (jsonStr) => {
470
+ const scan = self._ensureScanner();
471
+ if (scan === undefined) return byParsing(jsonStr);
472
+ if (scan === null) { _bindEntry(self, 'isValidJSON', byParsing); return byParsing(jsonStr); }
473
+ _bindEntry(self, 'isValidJSON', memoizable
474
+ ? (text) => {
475
+ if (typeof text !== 'string') return byParsing(text);
476
+ if (text === _memoText) return _memoVerdict;
477
+ const r = scan(text);
478
+ const verdict = r === -1 ? byParsing(text) : r === 1;
479
+ _memoText = text;
480
+ _memoVerdict = verdict;
481
+ return verdict;
221
482
  }
222
- if (
223
- typeof target !== "object" ||
224
- target === null ||
225
- Array.isArray(target)
226
- )
227
- return;
228
- const keys = Object.keys(target);
229
- for (let i = 0; i < keys.length; i++) {
230
- if (!allowed.has(keys[i])) delete target[keys[i]];
483
+ : (text) => {
484
+ if (typeof text !== 'string') return byParsing(text);
485
+ const r = scan(text);
486
+ if (r === -1) return byParsing(text);
487
+ return r === 1;
488
+ });
489
+ return self.isValidJSON(jsonStr);
490
+ };
491
+ // validateJSON gets the same short-circuit isValidJSON has. The verdict is
492
+ // a property of the text, and the scanner reads the text once and
493
+ // allocates nothing; above the simdjson threshold the path underneath
494
+ // encoded the whole document to a Buffer and called the native validator
495
+ // instead, which measured 340 microseconds against the scanner's 191 on a
496
+ // 149 KB config, the same against the published addon as against a local
497
+ // build.
498
+ //
499
+ // An accepted document stops at the scanner. A rejected one still has to
500
+ // produce errors, so it goes on to the path below, which is the
501
+ // rich-errors wrapper and everything under it; the flag only tells that
502
+ // path to skip an encode and a native call that would return false. Doing
503
+ // it the other way, returning errors from the inner function directly,
504
+ // would hand back errors that never passed through enrichment.
505
+ {
506
+ const validateByParsing = this.validateJSON;
507
+ const abortEarly = !!options.abortEarly;
508
+ this.validateJSON = (jsonStr) => {
509
+ const scan = self._ensureScanner();
510
+ if (scan === undefined) return validateByParsing(jsonStr);
511
+ if (scan === null) { _bindEntry(self, 'validateJSON', validateByParsing); return validateByParsing(jsonStr); }
512
+ _bindEntry(self, 'validateJSON', (text) => {
513
+ if (typeof text === 'string') {
514
+ const r = scan(text);
515
+ if (r === 1) return VALID_RESULT;
516
+ if (r === 0) {
517
+ if (abortEarly) return ABORT_EARLY_RESULT;
518
+ self._skipNativeFast = true;
519
+ try {
520
+ return validateByParsing(text);
521
+ } finally {
522
+ self._skipNativeFast = false;
523
+ }
524
+ }
231
525
  }
526
+ return validateByParsing(text);
232
527
  });
233
- }
234
- }
235
-
236
- // Always recurse into nested properties (they may have their own additionalProperties: false)
237
- for (const [key, prop] of Object.entries(schema.properties)) {
238
- if (prop && typeof prop === "object" && prop.properties) {
239
- collectRemovals(prop, actions, (path || []).concat(key));
240
- }
528
+ return self.validateJSON(jsonStr);
529
+ };
241
530
  }
242
- }
531
+ };
243
532
 
533
+ // The defaults, coercion and removal pass as generated source, which runs
534
+ // about twelve times faster than the closure mutators in validator-core.js.
535
+ // ata-validator/lite takes the closures.
244
536
  // Emit the in-place strip for one schema node and everything under its
245
537
  // `properties`. Scope matches collectRemovals(): object properties only, so
246
538
  // the two paths keep the same answer.
@@ -276,12 +568,117 @@ function emitRemovals(node, access, lines, depth, seen) {
276
568
  else lines.push(`if(${access}!==null&&typeof ${access}==='object'&&!Array.isArray(${access})){${body.join('\n')}}`);
277
569
  }
278
570
 
571
+ // The coercions the closure pass applies, emitted. Values: `number` and
572
+ // `integer` from numeric strings and booleans, `string` from numbers and
573
+ // booleans, `boolean` from "true"/"1" and "false"/"0".
574
+ const COERCIBLE_TYPES = new Set(['number', 'integer', 'string', 'boolean']);
575
+ function scalarCoercion(a, t) {
576
+ if (t === 'integer') return [`if(typeof ${a}==='string'){var _n=Number(${a});if(${a}!==''&&Number.isInteger(_n))${a}=_n}`, `if(typeof ${a}==='boolean')${a}=${a}?1:0`];
577
+ if (t === 'number') return [`if(typeof ${a}==='string'){var _n=Number(${a});if(${a}!==''&&!isNaN(_n))${a}=_n}`, `if(typeof ${a}==='boolean')${a}=${a}?1:0`];
578
+ if (t === 'string') return [`if(typeof ${a}==='number'||typeof ${a}==='boolean')${a}=String(${a})`];
579
+ return [`if(${a}==='true'||${a}==='1')${a}=true`, `if(${a}==='false'||${a}==='0')${a}=false`];
580
+ }
581
+ function isScalarCoercible(node) {
582
+ return !!(node && typeof node === 'object' && typeof node.type === 'string' && COERCIBLE_TYPES.has(node.type));
583
+ }
584
+ // Whether anything below `node` is coerced, as buildNodeCoercer decides it.
585
+ // Remembered per node for the build, since every enclosing node asks again.
586
+ function coercesInside(node, seen, memo) {
587
+ if (!node || typeof node !== 'object' || seen.has(node)) return false;
588
+ if (memo && memo.has(node)) return memo.get(node);
589
+ seen.add(node);
590
+ let found = false;
591
+ if (node.properties) {
592
+ for (const [key, prop] of Object.entries(node.properties)) {
593
+ if (key === '__proto__' || !prop || typeof prop !== 'object') continue;
594
+ if (isScalarCoercible(prop) || coercesInside(prop, seen, memo)) { found = true; break; }
595
+ }
596
+ }
597
+ if (!found && node.items && typeof node.items === 'object' && !Array.isArray(node.items)) {
598
+ found = isScalarCoercible(node.items) || coercesInside(node.items, seen, memo);
599
+ }
600
+ seen.delete(node);
601
+ if (memo) memo.set(node, found);
602
+ return found;
603
+ }
604
+ function emitCoercions(node, ov, lines, st, depth) {
605
+ if (st.seen.has(node)) { st.cycle = true; return; }
606
+ st.seen.add(node);
607
+ if (node.properties) {
608
+ for (const [key, prop] of Object.entries(node.properties)) {
609
+ // Coercion writes with plain assignment, which for a key named
610
+ // __proto__ rewrites the prototype instead. The raw value still goes
611
+ // through validation, so skipping is a refusal to coerce, not a hole.
612
+ if (key === '__proto__' || !prop || typeof prop !== 'object') continue;
613
+ const k = JSON.stringify(key);
614
+ const a = `${ov}[${k}]`;
615
+ if (isScalarCoercible(prop)) lines.push(...scalarCoercion(a, prop.type));
616
+ // Wrapping a lone value in an array is a top-level rule only, as it was.
617
+ else if (depth === 0 && prop.type === 'array' && st.arrayMode) lines.push(`if(${k} in ${ov}&&${a}!==undefined&&!Array.isArray(${a}))${a}=[${a}]`);
618
+ // A leaf, the common case, has nothing below it to coerce.
619
+ if ((prop.properties || prop.items) && coercesInside(prop, new Set(), st.memo)) {
620
+ const n = '_c' + st.n++;
621
+ lines.push(`{const ${n}=${a};if(typeof ${n}==='object'&&${n}!==null){`);
622
+ emitCoercions(prop, n, lines, st, depth + 1);
623
+ lines.push('}}');
624
+ }
625
+ }
626
+ }
627
+ const it = node.items;
628
+ if (it && typeof it === 'object' && !Array.isArray(it) && (isScalarCoercible(it) || coercesInside(it, new Set(), st.memo))) {
629
+ const i = '_i' + st.n++;
630
+ lines.push(`if(Array.isArray(${ov}))for(let ${i}=0;${i}<${ov}.length;${i}++){`);
631
+ if (isScalarCoercible(it)) lines.push(...scalarCoercion(`${ov}[${i}]`, it.type));
632
+ if (coercesInside(it, new Set(), st.memo)) {
633
+ const n = '_c' + st.n++;
634
+ lines.push(`{const ${n}=${ov}[${i}];if(typeof ${n}==='object'&&${n}!==null){`);
635
+ emitCoercions(it, n, lines, st, depth + 1);
636
+ lines.push('}}');
637
+ }
638
+ lines.push('}');
639
+ }
640
+ st.seen.delete(node);
641
+ }
642
+ function hasDefaultsInside(node, seen) {
643
+ if (!node || typeof node !== 'object' || !node.properties || seen.has(node)) return false;
644
+ seen.add(node);
645
+ let found = false;
646
+ for (const prop of Object.values(node.properties)) {
647
+ if (prop && typeof prop === 'object' && (prop.default !== undefined || hasDefaultsInside(prop, seen))) { found = true; break; }
648
+ }
649
+ seen.delete(node);
650
+ return found;
651
+ }
652
+ function emitDefaults(node, ov, lines, st) {
653
+ if (st.seen.has(node)) { st.cycle = true; return; }
654
+ st.seen.add(node);
655
+ for (const [key, prop] of Object.entries(node.properties || {})) {
656
+ if (!prop || typeof prop !== 'object') continue;
657
+ const k = JSON.stringify(key);
658
+ if (prop.default !== undefined) {
659
+ const def = JSON.stringify(prop.default);
660
+ // Assignment to a key named __proto__ hits the prototype setter
661
+ // instead of creating a property; defineProperty writes an own key.
662
+ lines.push(key === '__proto__'
663
+ ? `if(!Object.hasOwn(${ov},${k}))Object.defineProperty(${ov},${k},{value:${def},writable:true,enumerable:true,configurable:true})`
664
+ : `if(!Object.hasOwn(${ov},${k}))${ov}[${k}]=${def}`);
665
+ }
666
+ // Into an own property that holds an object, arrays included, as the
667
+ // closure pass walks it.
668
+ if (prop.properties && hasDefaultsInside(prop, new Set())) {
669
+ const n = '_d' + st.n++;
670
+ lines.push(`if(Object.hasOwn(${ov},${k})){const ${n}=${ov}[${k}];if(typeof ${n}==='object'&&${n}!==null){`);
671
+ emitDefaults(prop, n, lines, st);
672
+ lines.push('}}');
673
+ }
674
+ }
675
+ st.seen.delete(node);
676
+ }
677
+
279
678
  // Generate a fast preprocess function via codegen instead of closure arrays
280
679
  function buildPreprocessCodegen(schema, options) {
281
680
  if (typeof schema !== 'object' || schema === null || !schema.properties) return null;
282
681
  const lines = [];
283
- const props = schema.properties;
284
- const keys = Object.keys(props);
285
682
 
286
683
  // removeAdditional: strip unknown keys at every level the schema describes,
287
684
  // not just the top one. The closure path below (collectRemovals) always
@@ -293,2848 +690,157 @@ function buildPreprocessCodegen(schema, options) {
293
690
  emitRemovals(schema, 'd', lines, 0, new Set());
294
691
  }
295
692
 
296
- // coerceTypes: inline per property
297
- if (options.coerceTypes) {
298
- for (const [key, prop] of Object.entries(props)) {
299
- if (!prop || typeof prop !== 'object' || !prop.type) continue;
300
- // Coercion writes with plain assignment, which for a key named
301
- // __proto__ rewrites the prototype instead. The raw value still goes
302
- // through validation, so skipping is a refusal to coerce, not a hole.
303
- if (key === '__proto__') continue;
304
- const t = Array.isArray(prop.type) ? null : prop.type;
305
- if (!t) continue;
306
- const k = JSON.stringify(key);
307
- if (t === 'integer') {
308
- lines.push(`if(typeof d[${k}]==='string'){var _n=Number(d[${k}]);if(d[${k}]!==''&&Number.isInteger(_n))d[${k}]=_n}`);
309
- lines.push(`if(typeof d[${k}]==='boolean')d[${k}]=d[${k}]?1:0`);
310
- } else if (t === 'number') {
311
- lines.push(`if(typeof d[${k}]==='string'){var _n=Number(d[${k}]);if(d[${k}]!==''&&!isNaN(_n))d[${k}]=_n}`);
312
- lines.push(`if(typeof d[${k}]==='boolean')d[${k}]=d[${k}]?1:0`);
313
- } else if (t === 'string') {
314
- lines.push(`if(typeof d[${k}]==='number'||typeof d[${k}]==='boolean')d[${k}]=String(d[${k}])`);
315
- } else if (t === 'boolean') {
316
- lines.push(`if(d[${k}]==='true'||d[${k}]==='1')d[${k}]=true`);
317
- lines.push(`if(d[${k}]==='false'||d[${k}]==='0')d[${k}]=false`);
318
- } else if (t === 'array' && options.coerceTypes === 'array') {
319
- lines.push(`if(${k} in d&&d[${k}]!==undefined&&!Array.isArray(d[${k}]))d[${k}]=[d[${k}]]`);
320
- }
321
- }
322
- }
323
-
324
- // defaults: inline per property
325
- if (options.useDefaults !== false) {
326
- for (const [key, prop] of Object.entries(props)) {
327
- if (prop && typeof prop === 'object' && prop.default !== undefined) {
328
- const k = JSON.stringify(key);
329
- const def = JSON.stringify(prop.default);
330
- // Assignment to a key named __proto__ hits the prototype setter
331
- // instead of creating a property; defineProperty writes an own key.
332
- lines.push(key === '__proto__'
333
- ? `if(!Object.hasOwn(d,${k}))Object.defineProperty(d,${k},{value:${def},writable:true,enumerable:true,configurable:true})`
334
- : `if(!Object.hasOwn(d,${k}))d[${k}]=${def}`);
335
- }
336
- }
337
- }
693
+ // Coercion and defaults reach every depth the closure passes in
694
+ // validator-core.js reach, with the same rules in the same order, so the
695
+ // interpreted engine, which uses those passes, gives the same answer. Both
696
+ // used to stop at the top-level properties. A schema object that contains
697
+ // itself declines here, and the closure passes, which carry a guard for it,
698
+ // take the schema.
699
+ const st = { n: 0, seen: new Set(), cycle: false, arrayMode: options.coerceTypes === 'array', memo: new Map() };
700
+ if (options.coerceTypes) emitCoercions(schema, 'd', lines, st, 0);
701
+ // hasDefaultsInside answers without emitting; most schemas have no default,
702
+ // and the emitting walk allocates for every property it visits.
703
+ if (options.useDefaults !== false && hasDefaultsInside(schema, new Set())) emitDefaults(schema, 'd', lines, st);
704
+ if (st.cycle) return null;
338
705
 
339
706
  if (lines.length === 0) return null;
340
707
  // Data may legitimately be null or a non-object (e.g. a `['object','null']`
341
708
  // schema), so the per-property mutations must not run on it.
342
709
  lines.unshift(`if(d===null||typeof d!=='object')return`);
343
- try {
344
- return new Function('d', lines.join('\n'));
345
- } catch {
346
- return null;
347
- }
348
- }
349
-
350
- // Cloudflare Workers, Deno Deploy and pages under a strict Content-Security-
351
- // Policy refuse `new Function`. Probed once, lazily, because the answer cannot
352
- // change within a realm and the probe itself is a code generation attempt.
353
- let _codegenAvailable = null;
354
- function codegenAvailable() {
355
- if (_codegenAvailable === null) {
710
+ // The pass depends on property names, types, defaults and which objects
711
+ // are closed, not on the constraints the verdict checks, so routes that
712
+ // take the same shape with different limits (a page/limit querystring, an
713
+ // id param) emit the same source. Compiling it is most of what building the
714
+ // pass costs, so the function is kept by its source and compiled once. It
715
+ // holds no state: it rewrites the object it is given and nothing else.
716
+ const src = lines.join('\n');
717
+ let fn = _preprocessBySource.get(src);
718
+ if (fn === undefined) {
356
719
  try {
357
- _codegenAvailable = new Function('return 1')() === 1;
720
+ fn = new Function('d', src);
358
721
  } catch {
359
- _codegenAvailable = false;
722
+ fn = null;
360
723
  }
724
+ if (_preprocessBySource.size >= PREPROCESS_SOURCE_LIMIT) _preprocessBySource.clear();
725
+ _preprocessBySource.set(src, fn);
361
726
  }
362
- return _codegenAvailable;
363
- }
364
-
365
- // Schema compilation cache: same schema string -> reuse compiled functions
366
- const _compileCache = new Map();
367
-
368
- // Object identity cache: same schema object reference -> reuse entire compiled state
369
- // Skips JSON.stringify, cache lookup, and all setup. Near-zero cost for repeated schemas.
370
- const _identityCache = new WeakMap();
371
-
372
- const SIMDJSON_PADDING = 64;
373
- const VALID_RESULT = Object.freeze({ valid: true, errors: Object.freeze([]) });
374
- // How many calls a validator answers through its verdict function before its
375
- // validate() and validateJSON() compile the single-function hybrid.
376
- const HYBRID_TIER_CALLS = 64;
377
- const ABORT_EARLY_RESULT = Object.freeze({
378
- valid: false,
379
- errors: Object.freeze([Object.freeze({
380
- code: 'ATA9000',
381
- message: 'validation failed',
382
- keyword: '__abort_early__',
383
- path: '',
384
- })]),
385
- });
386
-
387
- // `_CP_LEN_SOURCE`, the safe-regex embed, and the AOT helpers that consume them
388
- // now live in `lib/aot.js` — keeping this file free of `fs`/`path`/`__dirname`
389
- // references so a default import never touches disk. The static AOT methods
390
- // further down lazily require `./lib/aot`, so they pay nothing until a user
391
- // calls `bundleStandalone`/`bundle`/etc.
392
-
393
- // Above this size, simdjson On Demand (selective field access) beats JSON.parse
394
- // (which must materialize the full JS object tree). Buffer.from + NAPI ~2x faster.
395
-
396
- // Rejection result with errors materialized on first read. The accessor
397
- // lives on the prototype so constructing one is a plain allocation; an
398
- // object-literal getter would create a closure and define an accessor
399
- // property on every rejection, which showed up as the single largest cost
400
- // on the rejection path. `toJSON` keeps JSON.stringify output identical to
401
- // the eager shape. Note for tests: deepStrictEqual against a plain object
402
- // compares prototypes; read `.errors` and compare that.
403
- class LazyRejection {
404
- constructor(build, data, buildRaw) {
405
- this.valid = false;
406
- this._build = build;
407
- this._data = data;
408
- this._errors = null;
409
- this._buildRaw = buildRaw;
410
- }
411
- toJSON() {
412
- return { valid: false, errors: this.errors };
413
- }
414
- // Raw shape for consumers that carry only message and path, such as the
415
- // Standard Schema bridge: schema order, no enrichment. Reading `errors`
416
- // afterwards still enriches through its own build.
417
- _ataRaw() {
418
- return this._buildRaw ? this._buildRaw(this._data) : this.errors;
419
- }
420
- }
421
- Object.defineProperty(LazyRejection.prototype, 'errors', {
422
- enumerable: true,
423
- configurable: true,
424
- get() {
425
- if (this._errors === null) this._errors = this._build(this._data);
426
- return this._errors;
427
- },
428
- });
429
-
430
- const SIMDJSON_THRESHOLD = 8192;
431
-
432
- // Resolve a JSON Schema path like "#/properties/name/type" to the schema object
433
- // that *contains* the failing keyword. Used by verbose mode to populate
434
- // `parentSchema` on validation errors. Returns undefined if the path can't be
435
- // walked (malformed pointer or missing intermediate node).
436
- function resolveSchemaByPath(rootSchema, schemaPath) {
437
- if (!schemaPath || typeof schemaPath !== 'string' || !schemaPath.startsWith('#')) {
438
- return undefined;
439
- }
440
- const stripped = schemaPath.slice(1);
441
- if (!stripped || stripped === '/') return rootSchema;
442
- const parts = stripped.split('/').filter(Boolean).map(s => s.replace(/~1/g, '/').replace(/~0/g, '~'));
443
- // The last segment is the keyword that failed (e.g. "type"); parentSchema is
444
- // the schema object that owns that keyword, so walk all but the last segment.
445
- let target = rootSchema;
446
- for (let i = 0; i < parts.length - 1; i++) {
447
- if (target == null || typeof target !== 'object') return undefined;
448
- target = target[parts[i]];
449
- }
450
- return target;
727
+ return fn;
451
728
  }
729
+ const _preprocessBySource = new Map();
730
+ const PREPROCESS_SOURCE_LIMIT = 4096;
452
731
 
453
- // Rank an error by walking its schemaPath through the schema object: at each
454
- // level the segment's index among the node's declared keys. Comparing ranks
455
- // lexicographically orders errors by keyword declaration order, which is the
456
- // order AJV emits and what schema authors read top to bottom. Segments that
457
- // cannot be resolved (cross-schema refs, normalized keys) end the walk; the
458
- // stable sort then keeps such errors in engine emission order.
459
- // The rank of a `schemaPath` under a given root is fixed: the schema does not
460
- // change between validations, so neither does the answer. It was recomputed for
461
- // every error of every failing document, and computing it is not cheap. Two
462
- // caches, both keyed on things that do not change:
463
- //
464
- // rootSchema -> schemaPath -> rank, so a path is walked once ever
465
- // node -> key -> its index, so the walk stops calling Object.keys and
466
- // scanning the result for a string
467
- //
468
- // A failing route sees the same handful of schemaPaths over and over, which is
469
- // what makes the first one worth having.
470
- const _rankCache = new WeakMap();
471
- const { rankFor: schemaOrderRank, ordinalFor: schemaOrdinal } = require('./lib/schema-order');
472
732
 
473
- // The rejection the rich-errors wrapper returns. `errors` is built on first
474
- // read and cached; `_ataRaw()` is the raw, schema-ordered list for consumers
475
- // that carry only message and path, such as the Standard Schema bridge.
476
- // Prototype accessors, not per-instance ones: see LazyRejection.
477
- class RichRejection {
478
- // `rawInput` is the JSON text validateJSON was given, or null for validate(data).
479
- // The position map it implies is built in the `errors` getter, not here: it is
480
- // a full walk of the document, it is only ever read through an error's
481
- // dataFrame, and building it on every rejection cost a caller that reads
482
- // `.valid` about 460 microseconds on a 50 KB document. Holding the text rather
483
- // than reading `self._lastRawInput` later also keeps the result independent of
484
- // what the instance does after this call returns.
485
- constructor(result, data, rawInput, self, root, enrich) {
486
- this.valid = false;
487
- this._result = result;
488
- this._data = data;
489
- this._rawInput = rawInput;
490
- this._self = self;
491
- this._root = root;
492
- this._enrich = enrich;
493
- this._cached = null;
494
- }
495
- toJSON() {
496
- return { valid: false, errors: this.errors };
497
- }
498
- _ataRaw() {
499
- let raw = this._result.errors || [];
500
- if (raw.length > 1) raw = sortErrorsBySchemaOrder(this._root, raw);
501
- return raw;
733
+ // parse() for the full package: a generated function that copies the keys the
734
+ // schema declares, behind the generated verdict. The refusals every build
735
+ // shares stay in validator-core.js; this is the part that needs the code
736
+ // generator.
737
+ function buildParse (self, decline, extended) {
738
+ const { cloneExprFor } = require('./lib/clone-emit');
739
+ const expr = cloneExprFor(self._schemaObj);
740
+ if (!expr) return decline('the set of keys to keep cannot be proven from the schema');
741
+ let copy;
742
+ try {
743
+ // eslint-disable-next-line no-new-func
744
+ copy = new Function('data', 'return ' + expr);
745
+ } catch {
746
+ return decline('code generation is not allowed here');
502
747
  }
503
- }
504
- Object.defineProperty(RichRejection.prototype, 'errors', {
505
- enumerable: true,
506
- configurable: true,
507
- get() {
508
- if (this._cached === null) {
509
- const self = this._self;
510
- const enrich = this._enrich;
511
- let raw = this._result.errors || [];
512
- if (raw.length > 1) raw = sortErrorsBySchemaOrder(this._root, raw);
513
- // The position map, resolved now that an error is actually being read.
514
- let positions = null;
515
- if (enrich && raw.length && this._rawInput != null) {
516
- positions = self._pos().targeted(this._rawInput, wantedPointersFor(raw));
517
- if (positions) self._posCache.reset();
748
+ self._ensureCompiled();
749
+ const verdict = extended ? (d) => self.isValidObject(d) : self._jsFn;
750
+ if (typeof self._jsFn !== 'function') return decline('the schema has no generated verdict function');
751
+ return (data) => {
752
+ if (!verdict(data)) {
753
+ const e = new Error('validation failed');
754
+ e.name = 'AtaValidationError';
755
+ let target = data;
756
+ if (self._mutatesInput) {
757
+ try { target = structuredClone(data); } catch { target = data; }
518
758
  }
519
- // One options object for the whole list, not one per error.
520
- const opts = enrich && raw.length
521
- ? {
522
- data: this._data,
523
- positions,
524
- schemaPositions: self._schemaPositions,
525
- schemaFile: self._source ? self._source.path : undefined,
526
- }
527
- : null;
528
- const cached = opts
529
- ? raw.map((e) => enrich(e, opts))
530
- // The v0.14 shape is a fixed key set; the ordering key the
531
- // generated code carries is dropped from it here.
532
- : raw.map(stripOrdinal);
533
- // Correlation is published, never applied. Both halves of a typo pair
534
- // stay in the array; `related` only says they are one mistake, so a
535
- // wrong pairing costs a sentence rather than a hidden violation.
536
- if (enrich && cached.length > 1) attachRelated(cached);
537
- // No diagnostic payload here. validate(data) is the library hot path,
538
- // and attaching one cost about 100 ns per rejection for a consumer
539
- // that never renders. The text path attaches it, and a renderer given
540
- // `{ data }` builds frames for object input on request.
541
- this._cached = cached;
759
+ e.errors = self.validate(target).errors;
760
+ throw e;
542
761
  }
543
- return this._cached;
544
- },
545
- });
546
-
547
- // The rejection validateJSON returns. Everything the text path adds over
548
- // validate(data), the value tree enrichment needs, the position map, the
549
- // diagnostic payload, happens on first access to `.errors`. Reading `.valid`
550
- // touches none of it. The JSON text is held here rather than read back off the
551
- // validator, so the result does not depend on what the instance does next.
552
- class LazyJsonRejection {
553
- constructor(result, jsonStr, self, enrich) {
554
- this.valid = false;
555
- this._result = result;
556
- this._jsonStr = jsonStr;
557
- this._self = self;
558
- this._enrich = enrich;
559
- this._cached = null;
560
- }
561
- toJSON() {
562
- return { valid: false, errors: this.errors };
563
- }
762
+ return copy(data);
763
+ };
564
764
  }
565
- Object.defineProperty(LazyJsonRejection.prototype, 'errors', {
566
- enumerable: true,
567
- configurable: true,
568
- get() {
569
- if (this._cached !== null) return this._cached;
570
- const self = this._self;
571
- const enrich = this._enrich;
572
- const jsonStr = this._jsonStr;
573
- // Reading the inner errors realizes the inner lazy layer, if there was one.
574
- const raw = this._result.errors || [];
575
- if (!raw.length) { this._cached = raw; return raw; }
576
-
577
- // The enrich pass plucks `received` from the value tree and the suggestion
578
- // engine (required-typo, format hints, coercion nudges) walks it too.
579
- let parsedData;
580
- try { parsedData = JSON.parse(jsonStr); } catch { parsedData = undefined; }
581
-
582
- // Errors the inner path already enriched carry a docUrl: only enrich() sets
583
- // one. `code` is not a safe signal, because branch-collapse attaches codes
584
- // to raw errors, and detecting on it left every collapsed oneOf/anyOf error
585
- // unenriched on the text path.
586
- if (!raw[0] || !raw[0].docUrl) {
587
- const positions = self._pos().targeted(jsonStr, wantedPointersFor(raw));
588
- // Declaration order, as validate() applies it; the text path used to
589
- // enrich in emission order.
590
- const ordered = raw.length > 1 ? sortErrorsBySchemaOrder(self._schemaObj, raw) : raw;
591
- const enrichOpts = {
592
- data: parsedData,
593
- positions,
594
- schemaPositions: self._schemaPositions,
595
- schemaFile: self._source ? self._source.path : undefined,
596
- };
597
- const enriched = ordered.map((e) => enrich(e, enrichOpts));
598
- if (enriched.length > 1) attachRelated(enriched);
599
- attachDiagnosticSource(enriched, {
600
- data: parsedData,
601
- text: jsonStr,
602
- positions,
603
- schema: self._schemaObj,
604
- mutatesInput: self._mutatesInput === true,
605
- });
606
- if (positions) self._posCache.reset();
607
- this._cached = enriched;
608
- return enriched;
609
- }
610
765
 
611
- // Already enriched, so the frames are usually attached too. Only a gap in
612
- // them is worth another walk of the document: resolving the map to discover
613
- // there was nothing to fill cost a second full walk on every rejection.
614
- if (raw.some((e) => e && !e.dataFrame)) {
615
- const positions = self._pos().targeted(jsonStr, wantedPointersFor(raw.filter((e) => e && !e.dataFrame)));
616
- if (positions) {
617
- for (const e of raw) {
618
- if (e && !e.dataFrame) {
619
- const path = e.path != null ? e.path : (e.instancePath || '');
620
- const p = positions[path];
621
- if (p) e.dataFrame = { byteOffset: p.byteOffset, length: p.length, line: p.line, col: p.col, text: p.text };
622
- }
623
- }
624
- self._posCache.reset();
625
- }
626
- }
627
- if (raw.length > 1) attachRelated(raw);
628
- attachDiagnosticSource(raw, {
629
- data: parsedData,
630
- text: jsonStr,
631
- schema: self._schemaObj,
632
- mutatesInput: self._mutatesInput === true,
633
- });
634
- this._cached = raw;
635
- return raw;
636
- },
766
+ // Everything that turns a schema into JavaScript source, registered with the
767
+ // core: the three code generators and the paths above, the generated
768
+ // preprocess pass, the JSON-text scanner, the parse() copy and the
769
+ // ahead-of-time bundle methods. parse() and the bundle methods load on first
770
+ // use.
771
+ core._registerCodegen({
772
+ jsCompiler,
773
+ installPaths: installCodegenPaths,
774
+ compileVerdict: installCodegenPaths.compileVerdict,
775
+ installScanner: installCodegenPaths.installScanner,
776
+ buildPreprocess: buildPreprocessCodegen,
777
+ buildParse,
778
+ aot: () => require('./lib/aot.js'),
637
779
  });
638
780
 
639
- // The pointers a set of errors will ask the position map about. lib/enrich-error
640
- // looks up the error's own path, and for an additional or unevaluated property
641
- // the child pointer named in `params`, unescaped, which is the form it asks for.
642
- // The escaped form goes in too: including a pointer that is never read costs
643
- // nothing, and the cache answers anything outside this set from the full map
644
- // rather than reporting no position.
645
- function wantedPointersFor (errors) {
646
- const wanted = new Set();
647
- for (const e of errors) {
648
- if (!e) continue;
649
- const path = e.path != null ? e.path : (e.instancePath || '');
650
- wanted.add(path);
651
- if (e.instancePath != null && e.instancePath !== path) wanted.add(e.instancePath);
652
- const params = e.params;
653
- const named = params && (params.additionalProperty || params.unevaluatedProperty);
654
- if (typeof named === 'string') {
655
- wanted.add(path + '/' + named);
656
- if (named.indexOf('~') !== -1 || named.indexOf('/') !== -1) {
657
- wanted.add(path + '/' + named.replace(/~/g, '~0').replace(/\//g, '~1'));
658
- }
659
- }
660
- }
661
- return wanted;
662
- }
663
-
664
- // A raw error without the `_o` ordering key, for the legacy error shape.
665
- function stripOrdinal(e) {
666
- if (e === null || typeof e !== 'object' || e._o === undefined) return e;
667
- const out = {};
668
- for (const k in e) if (k !== '_o') out[k] = e[k];
669
- return out;
670
- }
671
-
672
- // Errors in schema declaration order. Each error's key is its schemaPath's
673
- // pre-order ordinal in the root schema: written into the literal by the code
674
- // generator (`_o`), looked up once per path otherwise. Most rejections come
675
- // out already ordered, and those return without sorting or allocating.
676
- function sortErrorsBySchemaOrder(rootSchema, errors) {
677
- const n = errors.length;
678
- const keys = new Array(n);
679
- let sorted = true;
680
- let prev = -1;
681
- for (let i = 0; i < n; i++) {
682
- const e = errors[i];
683
- let o = typeof e._o === 'number' ? e._o : schemaOrdinal(rootSchema, e.schemaPath);
684
- // An error with no place in this document (an appended custom-keyword
685
- // error, a path into another schema) stays next to the error before it,
686
- // which is where the rank comparison left it too.
687
- if (o === null) o = prev < 0 ? 0 : prev;
688
- keys[i] = o;
689
- if (o < prev) sorted = false;
690
- prev = o;
691
- }
692
- if (sorted) return errors;
693
- // Error lists are short. A stable insertion sort over the integer keys
694
- // moves the errors in tandem with no comparator calls and no index array.
695
- const out = errors.slice();
696
- for (let i = 1; i < n; i++) {
697
- const k = keys[i];
698
- const e = out[i];
699
- let j = i - 1;
700
- while (j >= 0 && keys[j] > k) { keys[j + 1] = keys[j]; out[j + 1] = out[j]; j--; }
701
- keys[j + 1] = k;
702
- out[j + 1] = e;
703
- }
704
- return out;
705
- }
706
-
781
+ // The parts no Validator calls: TypeScript generation, the error renderers,
782
+ // the spec output format, the retry message for language models, the schema
783
+ // description and the suggestion helper. ata-validator/lite leaves them out.
784
+ // Each loads its module on first call. None of them is on the path to a first
785
+ // validated request, and together they were about a sixth of what requiring
786
+ // the package cost.
787
+ function toTypeScript (...a) { return require('./lib/ts-gen').toTypeScript(...a); }
788
+ function renderPretty (...a) { return require('./lib/render-pretty').renderPretty(...a); }
789
+ function renderCompact (...a) { return require('./lib/render-compact').renderCompact(...a); }
790
+ function toOutput (...a) { return require('./lib/output-format').toOutput(...a); }
791
+ function toRetryMessage (...a) { return require('./lib/retry-message').toRetryMessage(...a); }
792
+ function describeSchema (...a) { return require('./lib/describe-schema').describeSchema(...a); }
793
+ function renderJSON (...a) { return require('./lib/render-json').renderJSON(...a); }
707
794
 
708
- // Paths repeat across rejections of the same shape, so the parsed segment
709
- // list is cached per path string. Entries are frozen: the same array is
710
- // handed to every issue that names the path.
711
- //
712
- // The cache is bucketed by length and searched with ===, not keyed in a Map.
713
- // A path with an array index in it is concatenated afresh by every rejection,
714
- // and a Map has to hash each such string from scratch before it can look
715
- // anything up; that hashing was 30% of a Standard Schema rejection with
716
- // sixteen issues. Comparing against the few paths of the same length costs
717
- // less. Both levels are bounded so a stream of array indexes cannot grow it
718
- // without limit: a full bucket stops caching, too many lengths clear it.
719
- const _pathBuckets = new Map();
720
- const PATH_BUCKET_MAX = 32;
721
- const PATH_LENGTHS_MAX = 256;
722
- function parsePointerPath(path) {
723
- if (!path) return EMPTY_PATH;
724
- const n = path.length;
725
- let bucket = _pathBuckets.get(n);
726
- if (bucket !== undefined) {
727
- for (let i = 0; i < bucket.length; i += 2) if (bucket[i] === path) return bucket[i + 1];
728
- } else {
729
- if (_pathBuckets.size >= PATH_LENGTHS_MAX) _pathBuckets.clear();
730
- bucket = [];
731
- _pathBuckets.set(n, bucket);
732
- }
733
- const segs = Object.freeze(parsePointerPathUncached(path));
734
- if (bucket.length < PATH_BUCKET_MAX * 2) bucket.push(path, segs);
735
- return segs;
795
+ // Walk a JSON pointer (RFC 6901 escapes) into a data tree. Mirrors the helper
796
+ // inside lib/suggestions.js, kept local to avoid exporting an internal.
797
+ function _walkPointer (root, pointer) {
798
+ if (!pointer) return root;
799
+ const parts = pointer.replace(/^\//, '').split('/').map(s => s.replace(/~1/g, '/').replace(/~0/g, '~'));
800
+ let cur = root;
801
+ for (const p of parts) { if (cur == null) return undefined; cur = cur[p]; }
802
+ return cur;
736
803
  }
737
- const EMPTY_PATH = Object.freeze([]);
738
804
 
739
- function parsePointerPathUncached(path) {
740
- // One pass, no intermediate arrays. Per Standard Schema V1 an array index
741
- // is emitted as a number and an object key as a string; a segment is an
742
- // index when it is all digits with no leading zero.
743
- const out = [];
744
- const n = path.length;
745
- let start = 1;
746
- for (let i = 1; i <= n; i++) {
747
- if (i !== n && path.charCodeAt(i) !== 47) continue;
748
- if (i > start) {
749
- let seg = path.slice(start, i);
750
- if (seg.indexOf('~') >= 0) seg = seg.replace(/~1/g, '/').replace(/~0/g, '~');
751
- const c0 = seg.charCodeAt(0);
752
- let numeric = c0 >= 48 && c0 <= 57 && (seg.length === 1 || c0 !== 48);
753
- if (numeric) {
754
- for (let k = 1; k < seg.length; k++) {
755
- const c = seg.charCodeAt(k);
756
- if (c < 48 || c > 57) { numeric = false; break; }
757
- }
758
- }
759
- out.push({ key: numeric ? Number(seg) : seg });
805
+ // Post-hoc suggestion enrichment for AOT-compiled validators. The standalone
806
+ // modules do not embed the suggestion engine, so consumers who want
807
+ // suggestions pass the error array through this after validation.
808
+ function attachSuggestions (errors, data) {
809
+ if (!errors) return errors;
810
+ const { suggestFor } = require('./lib/suggestions');
811
+ const { reprValue } = require('./lib/enrich-error');
812
+ for (const e of errors) {
813
+ if (!e || e.suggestion) continue;
814
+ let received = e.received;
815
+ if (received === undefined && data !== undefined) {
816
+ const ptr = e.instancePath != null ? e.instancePath : (e.path || '');
817
+ const raw = _walkPointer(data, ptr);
818
+ if (raw !== undefined || ptr === '') received = reprValue(raw);
760
819
  }
761
- start = i + 1;
762
- }
763
- return out;
764
- }
765
-
766
- function createPaddedBuffer(jsonStr) {
767
- if (typeof Buffer === 'undefined') throw new Error('createPaddedBuffer requires Node.js Buffer');
768
- const jsonBuf = Buffer.from(jsonStr);
769
- const padded = Buffer.allocUnsafe(jsonBuf.length + SIMDJSON_PADDING);
770
- jsonBuf.copy(padded);
771
- padded.fill(0, jsonBuf.length);
772
- return { buffer: padded, length: jsonBuf.length };
773
- }
774
-
775
- // Deep-clone a value, copying own symbol keys by reference at every level.
776
- // Arrays and plain objects are cloned recursively; primitives, RegExp,
777
- // functions, and other non-plain values are returned as-is. Symbol values
778
- // (e.g. refinement lists, OPTIONAL markers) are owned by the caller's builder
779
- // and sharing them is correct — they are never mutated by normalization.
780
- function _deepCloneWithSymbols(v) {
781
- if (v === null || typeof v !== 'object') return v;
782
- if (Array.isArray(v)) {
783
- const a = new Array(v.length);
784
- for (let i = 0; i < v.length; i++) a[i] = _deepCloneWithSymbols(v[i]);
785
- return a;
786
- }
787
- // Only clone plain objects (skip RegExp, Date, etc.).
788
- if (Object.getPrototypeOf(v) !== Object.prototype && Object.getPrototypeOf(v) !== null) return v;
789
- const out = Object.create(null);
790
- for (const k of Object.keys(v)) Object.defineProperty(out, k, { value: _deepCloneWithSymbols(v[k]), writable: true, enumerable: true, configurable: true });
791
- for (const sym of Object.getOwnPropertySymbols(v)) out[sym] = v[sym];
792
- return Object.setPrototypeOf(out, Object.prototype);
793
- }
794
-
795
- // Normalize a caller-provided schema without mutating the original.
796
- // Clones only when normalization would change the object (draft-07 keys
797
- // present or nullable fields present). Internal-only — not exported.
798
- function _normalizeCallerSchema(s, inheritDraft7) {
799
- const declares = s && typeof s === 'object' && s.$schema !== undefined
800
- const needsDraft7 = declares
801
- ? (s.$schema === 'http://json-schema.org/draft-07/schema#' || s.$schema === 'http://json-schema.org/draft-07/schema')
802
- : !!inheritDraft7
803
- // One walk answers whether there is anything to do. Almost always there is
804
- // not, and then the serialize, clone, normalize, serialize, compare below is
805
- // work spent to find that out. The walk over-reports rather than under, so a
806
- // schema it clears is one no normalizer would have touched;
807
- // `tests/test_schema_scan.js` holds that direction against the whole suite.
808
- if (!needsNormalization(s, needsDraft7)) return s
809
-
810
- const str = JSON.stringify(s)
811
- const copy = _deepCloneWithSymbols(s)
812
- if (needsDraft7) normalizeDraft7(copy, true)
813
- normalizeNullable(copy)
814
- normalizeExclusiveBounds(copy)
815
- // Return original when normalization produced no change, copy otherwise.
816
- // Kept even though the walk has already said there is work, so that a walk
817
- // which over-reports still returns exactly what it returned before.
818
- // Change-detection uses JSON content only; symbols do not affect it.
819
- return JSON.stringify(copy) === str ? s : copy
820
- }
821
-
822
- // The identity a document is registered under. Draft-07 ignores every
823
- // keyword sitting next to `$ref`, so normalization drops them, `$id` among
824
- // them: that is the right reading for evaluation, where the reference
825
- // resolves against the retrieval URI rather than the declared `$id`. It is
826
- // the wrong reading for registration, since `$id` is how the caller names
827
- // the document. So the identity is read from the normalized copy first and
828
- // from what the caller passed second. A bare-fragment `$id` is a draft-07
829
- // anchor rather than a document identity, and normalization has already
830
- // turned it into `$anchor`, so it is not used here.
831
- function declaredId(original, normalized) {
832
- const n = normalized && typeof normalized === 'object' ? normalized.$id : undefined
833
- if (typeof n === 'string' && n !== '') return n
834
- const o = original && typeof original === 'object' ? original.$id : undefined
835
- if (typeof o === 'string' && o !== '' && o[0] !== '#') return o
836
- return undefined
837
- }
838
-
839
- // `inheritDraft7` is true when the root schema is draft-07: a retrieved
840
- // document that declares no dialect is read under the root's draft.
841
- // The map is derived entirely from what the caller passed, so the same
842
- // `schemas` gives the same map. A server building one validator per route over
843
- // a shared registry rebuilt it once per route, normalizing and re-reading the
844
- // `$id` of every registered schema each time. Keyed by the registry object,
845
- // and by the draft it is read under, since that changes what normalization
846
- // does to a document which declares no dialect of its own.
847
- //
848
- // Validators share the returned map, so anything that mutates one calls
849
- // `_ownSchemaMap()` first. There are two such places: registering the vendored
850
- // meta-schemas during compilation, and `addSchema()`.
851
- const _schemaMapCache = new WeakMap()
852
-
853
- function buildSchemaMap(schemas, inheritDraft7) {
854
- if (!schemas) return null
855
- const byDraft = _schemaMapCache.get(schemas)
856
- if (byDraft) {
857
- const hit = byDraft[inheritDraft7 ? 1 : 0]
858
- if (hit) return hit
820
+ const probe = received !== undefined && e.received === undefined
821
+ ? Object.assign({}, e, { received })
822
+ : e;
823
+ const s = suggestFor(probe, data);
824
+ if (s) e.suggestion = s;
859
825
  }
860
- const map = _buildSchemaMap(schemas, inheritDraft7)
861
- const slot = byDraft || [null, null]
862
- slot[inheritDraft7 ? 1 : 0] = map
863
- if (!byDraft) _schemaMapCache.set(schemas, slot)
864
- return map
826
+ // AOT modules import nothing, so this is their only route to a frame. The
827
+ // caller holds the original object and ran no preprocessing through here.
828
+ require('./lib/diagnostic-source').setDiagnosticSource(errors, { data, mutatesInput: false });
829
+ return errors;
865
830
  }
866
831
 
867
- function _buildSchemaMap(schemas, inheritDraft7) {
868
- const map = new Map()
869
- if (Array.isArray(schemas)) {
870
- for (const s of schemas) {
871
- const normalized = _normalizeCallerSchema(s, inheritDraft7)
872
- const id = declaredId(s, normalized)
873
- if (!id) throw new Error('Schema in schemas option must have $id')
874
- map.set(id, normalized)
875
- }
876
- } else {
877
- for (const [key, s] of Object.entries(schemas)) {
878
- const normalized = _normalizeCallerSchema(s, inheritDraft7)
879
- // A retrieved document is addressable both by the URI it was registered
880
- // under and by the $id it declares. Registering only the $id makes
881
- // references to the retrieval URI unresolvable.
882
- map.set(key, normalized)
883
- const id = declaredId(s, normalized)
884
- if (id && id !== key) map.set(id, normalized)
885
- }
886
- }
887
- return map
888
- }
889
-
890
- // A schema which names a custom meta-schema in `$schema` is written against
891
- // whatever dialect that meta-schema declares. A keyword from a vocabulary the
892
- // dialect does not have is not part of the dialect, so it is an unknown
893
- // keyword and does not apply. Removing it here means every engine sees the
894
- // same schema and none of them needs to know about vocabularies.
895
- //
896
- // Only the root is consulted. A subschema naming its own `$schema` is its own
897
- // resource under its own dialect, and the walk stops there rather than
898
- // applying this dialect's answer to it.
899
- function _applyVocabularies(schemaObj, original, schemaMap) {
900
- if (!schemaObj || typeof schemaObj !== 'object') return schemaObj
901
- const declared = schemaObj.$schema
902
- if (typeof declared !== 'string') return schemaObj
903
- const enabled = enabledKeywords(schemaMap.get(declared))
904
- if (!enabled) return schemaObj
905
- // `original` is the caller's own object when it reached here unchanged, and
906
- // that one is never mutated.
907
- const copy = schemaObj === original
908
- ? _deepCloneWithSymbols(schemaObj)
909
- : schemaObj
910
- return stripDisabledKeywords(copy, enabled)
911
- }
912
-
913
- // Compile-cache key for a root schema plus its external schemas. Must include
914
- // the external schema CONTENT, not just their $ids: two validators can share a
915
- // root schema string and the same $id while pointing that $id at different
916
- // schemas (separate app instances, test suites, multi-tenant). Keying on $id
917
- // alone reuses the wrong compiled validator and silently mis-validates.
918
- function compileCacheKey(schemaStr, schemaMap) {
919
- if (!schemaMap || schemaMap.size === 0) return schemaStr
920
- const parts = []
921
- for (const [id, s] of schemaMap) parts.push(id + '=' + JSON.stringify(s))
922
- parts.sort()
923
- return schemaStr + '\0' + parts.join('\0')
924
- }
925
-
926
- // Resolve a relative URI ref against a base URI
927
- function resolveRelativeRef(ref, baseId) {
928
- if (!baseId || ref.includes('://') || ref.startsWith('#')) return ref
929
- const lastSlash = baseId.lastIndexOf('/')
930
- if (lastSlash < 0) return ref
931
- return baseId.substring(0, lastSlash + 1) + ref
932
- }
933
-
934
- // Resolve a cross-schema $ref to its target schema for preprocessing purposes.
935
- // Handles whole-schema refs (`shared#`), relative-id matching, and JSON pointer
936
- // fragments (`shared#/properties/id`). Returns null for local-only refs or when
937
- // the target cannot be found. Used only to read `type`/`properties` for
938
- // coercion/defaults/removeAdditional, never for validation.
939
- function resolveRefForPreprocess(ref, schemaMap) {
940
- if (!schemaMap || schemaMap.size === 0 || typeof ref !== 'string') return null
941
- const hashIdx = ref.indexOf('#')
942
- const baseId = hashIdx >= 0 ? ref.slice(0, hashIdx) : ref
943
- const fragment = hashIdx >= 0 ? ref.slice(hashIdx + 1) : ''
944
- if (!baseId) return null
945
- let base = null
946
- if (schemaMap.has(baseId)) base = schemaMap.get(baseId)
947
- else if (!ref.includes('://')) {
948
- for (const [id, s] of schemaMap) {
949
- if (id.endsWith('/' + baseId)) { base = s; break }
950
- }
951
- }
952
- if (!base) return null
953
- if (!fragment) return base
954
- let target = base
955
- for (const part of fragment.split('/')) {
956
- if (part === '') continue
957
- if (target == null || typeof target !== 'object') return null
958
- target = target[part.replace(/~1/g, '/').replace(/~0/g, '~')]
959
- }
960
- return target == null ? null : target
961
- }
962
-
963
- // Preprocessing (coerce/defaults/removeAdditional) reads `schema.properties` and
964
- // each property's `type`. When the data shape lives behind a cross-schema $ref
965
- // (a whole-schema ref like Fastify's `params: { $ref: 'shared#' }`, or a
966
- // property ref like `{ id: { $ref: 'shared#/properties/id' } }`), follow the
967
- // ref so the preprocessor can see the referenced shape. Returns the schema with
968
- // such refs resolved, cloning only when a substitution is made.
969
- function resolveSchemaForPreprocess(schema, schemaMap) {
970
- if (!schema || typeof schema !== 'object' || !schemaMap || schemaMap.size === 0) return schema
971
- let s = schema
972
- // Whole-schema ref (only when it has no own properties, to avoid dropping
973
- // sibling keywords on schemas that mix $ref with properties).
974
- if (s.$ref && !s.properties) {
975
- const t = resolveRefForPreprocess(s.$ref, schemaMap)
976
- if (t && typeof t === 'object') s = t
977
- }
978
- if (!s.properties) return s
979
- // Property-level refs: substitute the resolved target so coercion sees `type`.
980
- let cloned = null
981
- for (const key of Object.keys(s.properties)) {
982
- const p = s.properties[key]
983
- if (p && typeof p === 'object' && p.$ref && !p.type) {
984
- const t = resolveRefForPreprocess(p.$ref, schemaMap)
985
- if (t && typeof t === 'object') {
986
- if (!cloned) { cloned = Object.assign({}, s); cloned.properties = Object.assign({}, s.properties) }
987
- cloned.properties[key] = t
988
- }
989
- }
990
- }
991
- return cloned || s
992
- }
993
-
994
- // `_schemaObj` and `_usesKeywords` are materialized together on first read:
995
- // the caller's schema normalized on a clone (the caller's object is never
996
- // touched), `format` stripped under `assertFormat: false`, and the custom
997
- // keyword scan. The accessors then step aside for own data properties, so
998
- // every later read is a plain field.
999
- function _materializeSchema(self) {
1000
- const raw = self._rawSchema;
1001
- const options = self._options;
1002
- let schemaObj = _normalizeCallerSchema(raw);
1003
- const isCallers = self._rawIsCallers && schemaObj === raw;
1004
- if (options.assertFormat === false) {
1005
- schemaObj = stripFormatAssertions(isCallers ? _deepCloneWithSymbols(schemaObj) : schemaObj);
1006
- }
1007
- const usesKeywords = self._keywords !== null && schemaUsesKeywords(schemaObj, self._keywords);
1008
- Object.defineProperty(self, '_schemaObj', { value: schemaObj, writable: true, configurable: true, enumerable: true });
1009
- Object.defineProperty(self, '_usesKeywords', { value: usesKeywords, writable: true, configurable: true, enumerable: true });
1010
- Object.defineProperty(self, '_schemaIsCallers', { value: isCallers && schemaObj === raw, writable: true, configurable: true, enumerable: true });
1011
- return schemaObj;
1012
- }
1013
-
1014
- class Validator {
1015
- constructor(schema, opts) {
1016
- const options = opts || {};
1017
-
1018
- // Ultra-fast path: same schema object reference -> return cached instance
1019
- // JS constructor returning an object makes `new` return that object
1020
- // Cost: one WeakMap lookup. No property copy, no setup, nothing.
1021
- if (!opts && typeof schema === "object" && schema !== null) {
1022
- const hit = _identityCache.get(schema);
1023
- if (hit) return hit;
1024
- }
1025
-
1026
- // The schema is not walked here. Normalization (draft-07 rewrites,
1027
- // nullable, `assertFormat: false`) and the scan that decides whether any
1028
- // of it is needed run on the first read of `_schemaObj`, which is the
1029
- // first compile. Construction is the object and its fields; a server
1030
- // building a validator per request pays nothing for a schema it never
1031
- // uses, and a benchmark timing construction measures construction.
1032
- // When schema is a string, JSON.parse already produces a fresh object.
1033
- const raw = typeof schema === "string" ? JSON.parse(schema) : schema;
1034
- const rootIsDraft7 = !!(raw && typeof raw === 'object' && typeof raw.$schema === 'string' &&
1035
- (raw.$schema === 'http://json-schema.org/draft-07/schema#' || raw.$schema === 'http://json-schema.org/draft-07/schema'));
1036
-
1037
- // Built here rather than below because `$vocabulary` is resolved against
1038
- // it, and that resolution waits until compilation so a meta-schema
1039
- // registered by addSchema() still counts.
1040
- const shared = buildSchemaMap(options.schemas, rootIsDraft7);
1041
- const schemaMap = shared || new Map();
1042
- this._schemaMapShared = shared !== null;
1043
- this._vocabulariesApplied = false;
1044
-
1045
- // Custom keywords, normalized once. `_usesKeywords` (resolved with the
1046
- // schema) is what routes the schema to the interpreted engine and keeps
1047
- // it out of the shared compile cache; a schema that registers keywords
1048
- // but uses none of them takes the ordinary path.
1049
- this._keywords = normalizeKeywords(options.keywords);
1050
-
1051
- this._schemaStr = null; // lazy: computed on first use
1052
- this._rawSchema = raw;
1053
- this._rawIsCallers = typeof schema !== "string";
1054
- this._options = options;
1055
- this._noOpts = !opts;
1056
- // engine: 'interpreter' keeps this validator off code generation: no
1057
- // `new Function`, no shared compile cache, the eval-free interpreted
1058
- // engine answers validate(), isValidObject() and validateJSON(). For a
1059
- // schema that arrives from outside the trust boundary (a plugin's
1060
- // declared config shape, a tenant's upload), where turning it into source
1061
- // is not an acceptable execution model. ATA_FORCE_NAPI does this for the
1062
- // whole process; the option does it for one validator. A misspelling
1063
- // must not fall through to codegen, so anything else is refused.
1064
- if (options.engine !== undefined && options.engine !== 'auto' && options.engine !== 'interpreter') {
1065
- throw new TypeError("engine must be 'auto' or 'interpreter', got " + JSON.stringify(options.engine));
1066
- }
1067
- this._interpretOnly = options.engine === 'interpreter';
1068
- this._initialized = false;
1069
- this._nativeReady = false;
1070
- this._compiled = null;
1071
- this._fastSlot = -1;
1072
- this._jsFn = null;
1073
- this._engine = undefined;
1074
- this._preprocess = null;
1075
- this._applyDefaults = null;
1076
-
1077
- // Schema map for cross-schema $ref resolution
1078
- this._schemaMap = schemaMap;
1079
-
1080
- // User-supplied format checkers: { formatName: (value) => boolean }.
1081
- // Looked up at runtime when a schema references a format the built-in
1082
- // registry does not know about.
1083
- this._userFormats = options.formats || null;
1084
-
1085
- // Verbose mode: when on, errors carry parentSchema (the schema object that
1086
- // produced the error). Matches ajv's `verbose: true` behavior.
1087
- this._verbose = !!options.verbose;
1088
-
1089
- // strictSchema: authoring-time checks, off by default. A mistyped keyword
1090
- // is the one schema mistake that fails open: to every dialect `maxLenght`
1091
- // is an annotation, so the constraint the author meant is simply absent
1092
- // and previously invalid data validates. `true` throws here, at
1093
- // construction, with every finding; `'log'` reports through
1094
- // `options.logger` or the console and continues. The check runs on the
1095
- // schema as written, before any normalization touches it.
1096
- if (options.strictSchema === true || options.strictSchema === 'log') {
1097
- const { checkSchemaStrict } = require('./lib/strict-check');
1098
- const problems = checkSchemaStrict(schema, { userKeywords: options.keywords || null });
1099
- if (problems.length > 0) {
1100
- const text = problems.map((x) => `strict mode: ${x.message} at ${x.path}`).join('\n');
1101
- if (options.strictSchema === true) {
1102
- throw new Error(text);
1103
- }
1104
- const logger = options.logger;
1105
- if (logger !== false) {
1106
- const warn = logger && typeof logger.warn === 'function' ? logger.warn.bind(logger) : console.warn;
1107
- warn(text);
1108
- }
1109
- }
1110
- }
1111
-
1112
- // richErrors: default true. Only the literal `false` opts back into the
1113
- // v0.14 error shape (no code/expected/received/docUrl, no aliases).
1114
- this._richErrors = options && options.richErrors === false ? false : true;
1115
-
1116
- // Optional schema source descriptor. When supplied, the renderer pipeline
1117
- // can attach a `schemaSource` frame to enriched errors.
1118
- this._source = options && options.source && typeof options.source === 'object'
1119
- ? { path: String(options.source.path || ''), content: String(options.source.content || '') }
1120
- : null;
1121
-
1122
- // Build a JSON pointer -> position map for the schema text once at
1123
- // construction so each runtime error can resolve `schemaSource` without
1124
- // re-scanning the source on every validate() call.
1125
- if (this._source) {
1126
- const { buildPositionMap } = require('./lib/source-positions');
1127
- this._schemaPositions = buildPositionMap(this._source.content);
1128
- } else {
1129
- this._schemaPositions = null;
1130
- }
1131
-
1132
- // Per-validate data position cache. Populated by validateJSON before
1133
- // dispatching to inner validate(); consulted by the rich-error wrap
1134
- // to attach dataFrame entries to each enriched error.
1135
- this._posCache = null; // created by _pos() on first use, only the JSON text path needs it
1136
- this._lastRawInput = null;
1137
- // undefined: not built yet. null: this schema has no scanner.
1138
- this._scanner = undefined;
1139
- // Checks a wrapper registered through _extendChecks. Declared here so
1140
- // registering one does not change the instance's shape.
1141
- this._verdictTail = null;
1142
- this._validateTail = null;
1143
- this._entryExt = null;
1144
-
1145
- // Public methods start as memoized accessors on the prototype; nothing is
1146
- // allocated per instance until one is first read. See _defineLazyMethod
1147
- // below the class.
1148
-
1149
- // "~standard" (Standard Schema V1) is a lazy prototype accessor too;
1150
- // see below the class. Consumers only pay for it if they read it.
1151
-
1152
- // The identity cache, which lets a later `new Validator(sameSchema)` return
1153
- // this instance, is filled on the first compile rather than here. A WeakMap
1154
- // entry is an ephemeron the collector has to trace separately, and setting
1155
- // one cost about 780 ns against 150 for the rest of this constructor, five
1156
- // times over for an instance that may never validate anything. Once an
1157
- // instance has compiled, the shortcut behaves as before.
1158
- }
1159
-
1160
- // `$vocabulary` says which keywords the dialect has, and answering needs the
1161
- // meta-schema, which addSchema() may only have registered just now. Run once,
1162
- // before anything reads the schema, and before `_schemaStr` is computed from
1163
- // it. After this addSchema() is refused, so the answer cannot go stale.
1164
- // Whether validation is preceded by a pass that rewrites the input:
1165
- // coercion, removal of undeclared keys, or filling in defaults. The verdict
1166
- // methods have to take the same path when it is, so the quick bindings that
1167
- // answer from the compiled function alone are not used for these validators.
1168
- _needsPreprocess() {
1169
- const o = this._options;
1170
- if (o.coerceTypes || o.removeAdditional) return true;
1171
- if (o.useDefaults === false) return false;
1172
- if (!this._schemaStr) this._schemaStr = JSON.stringify(this._schemaObj);
1173
- return this._schemaStr.includes('"default"');
1174
- }
1175
-
1176
- _pos() {
1177
- return this._posCache || (this._posCache = _createPosCache());
1178
- }
1179
-
1180
- _ensureVocabularies() {
1181
- if (this._vocabulariesApplied) return;
1182
- this._vocabulariesApplied = true;
1183
- const stripped = _applyVocabularies(
1184
- this._schemaObj,
1185
- this._schemaIsCallers ? this._schemaObj : null,
1186
- this._schemaMap,
1187
- );
1188
- if (stripped !== this._schemaObj) {
1189
- this._schemaObj = stripped;
1190
- this._schemaStr = null;
1191
- }
1192
- }
1193
-
1194
- _ensureCompiled() {
1195
- if (this._initialized) return;
1196
- this._ensureVocabularies();
1197
- this._initialized = true;
1198
-
1199
- const schemaObj = this._schemaObj;
1200
- const options = this._options;
1201
-
1202
- // Lazy stringify — only computed here, not in constructor
1203
- if (!this._schemaStr) this._schemaStr = JSON.stringify(schemaObj);
1204
-
1205
- // A $ref to a meta-schema resolves from the vendored copies, so
1206
- // "validate this schema against its dialect" needs no network and no
1207
- // caller-supplied registry. Only schemas that mention json-schema.org in a
1208
- // reference pay for the lookup.
1209
- if (this._schemaStr.includes('json-schema.org/draft')) {
1210
- const { METASCHEMAS } = require('./lib/metaschemas');
1211
- this._ownSchemaMap();
1212
- for (const [id, meta] of METASCHEMAS) {
1213
- const bare = id.replace(/#$/, '');
1214
- for (const key of [id, bare, bare + '#', bare.replace(/^https:/, 'http:'), bare.replace(/^http:/, 'https:')]) {
1215
- if (!this._schemaMap.has(key)) this._schemaMap.set(key, meta);
1216
- }
1217
- }
1218
- }
1219
-
1220
- // Check cache first -- reuse compiled functions for same schema
1221
- const sm = this._schemaMap.size > 0 ? this._schemaMap : null;
1222
- const mapKey = compileCacheKey(this._schemaStr, this._schemaMap);
1223
- var _forceNapi = this._interpretOnly || (typeof process !== 'undefined' && process.env && process.env.ATA_FORCE_NAPI);
1224
- // Custom formats are JS functions: bypass the compile cache since they can
1225
- // differ between validators that share the same schema string. An
1226
- // interpreter-only validator never touches it either way: a function a
1227
- // trusted validator compiled for the same schema string must not answer
1228
- // for it.
1229
- const cached = (this._userFormats || this._usesKeywords || _forceNapi) ? null : _compileCache.get(mapKey);
1230
- let jsFn, jsCombinedFn, jsErrFn, _isCodegen = false;
1231
- // v1 removes the bookending requirement for $dynamicRef. Only the
1232
- // interpreted engine implements that; the JS compiler and the native
1233
- // engine both resolve the 2020-12 way, so a v1 schema using the keyword
1234
- // goes to the interpreter rather than being validated under the wrong
1235
- // dialect. Schemas without $dynamicRef are unaffected: v1 and 2020-12
1236
- // agree on everything else ata implements.
1237
- this._v1Dynamic =
1238
- isV1Dialect(schemaObj) &&
1239
- (this._schemaStr.includes('"$dynamicRef"') || this._schemaStr.includes('"$dynamicAnchor"'));
1240
- //
1241
- // Where source cannot be turned into a function, neither JS path is usable
1242
- // either. The closure path does not call `new Function` itself, so it
1243
- // survives the block and would quietly handle schemas it gets wrong; the
1244
- // interpreted engine is both eval-free and more correct, so go straight
1245
- // there. The forced case is decided before the probe: the probe is a
1246
- // `new Function` too, and a validator that promised none must not run it.
1247
- if (_forceNapi || this._v1Dynamic || this._usesKeywords || !codegenAvailable()) {
1248
- jsFn = null; jsCombinedFn = null; jsErrFn = null;
1249
- // `full` separates an entry that holds every compiled function from one
1250
- // the verdict-only fast path seeded, where `combined` and `errFn` are null
1251
- // because nothing has tried to build them yet. Both halves of that
1252
- // distinction are null, and reading the second as the first costs this
1253
- // schema its generated error function for the life of the process.
1254
- } else if (cached && cached.jsFn !== undefined) {
1255
- // `full` says the error and combined functions exist too. An entry
1256
- // without it still carries a verdict function worth reusing; the pair is
1257
- // built by _buildErr/_buildCombined below when something asks, and the
1258
- // entry is upgraded then. `undefined` in `combined`/`errFn` means not
1259
- // built yet; `null` means the compiler declined. Those two must never
1260
- // blur: reading the first as the second is the bug this cache had once
1261
- // already, and it silently cost schemas their generated error function.
1262
- jsFn = cached.jsFn;
1263
- jsCombinedFn = cached.combined;
1264
- jsErrFn = cached.errFn;
1265
- _isCodegen = !!cached.isCodegen;
1266
- this._engine = _isCodegen ? 'codegen' : jsFn ? 'closure' : null;
1267
- } else {
1268
- const uf = this._userFormats;
1269
- const _cgFn = compileToJSCodegen(schemaObj, sm, uf);
1270
- jsFn = _cgFn || compileToJS(schemaObj, null, sm);
1271
- // Only the verdict is compiled here. The error and combined generators
1272
- // are the other two thirds of a cold first call (8.2, 10.4 and 7.8 ms on
1273
- // a 120-property config schema, most of it V8 compiling each generator
1274
- // the first time it is entered), and a caller that never reads an error
1275
- // never needs them. _buildErr/_buildCombined compile them on demand.
1276
- jsCombinedFn = undefined;
1277
- jsErrFn = undefined;
1278
- _isCodegen = !!_cgFn;
1279
- this._engine = _cgFn ? 'codegen' : jsFn ? 'closure' : null;
1280
- if (!uf) {
1281
- _compileCache.set(mapKey, { jsFn, combined: undefined, errFn: undefined, isCodegen: _isCodegen, full: false });
1282
- }
1283
- }
1284
- this._jsFn = jsFn;
1285
- if (this._engine === undefined) this._engine = null;
1286
-
1287
- // Data mutators -- try codegen first (12x faster), fallback to closure arrays.
1288
- // Follow cross-refs so coercion/defaults/removeAdditional see the referenced
1289
- // shape (e.g. Fastify `params: { $ref: 'shared#' }` or property refs like
1290
- // `{ id: { $ref: 'shared#/properties/id' } }`).
1291
- const preprocessSchema = resolveSchemaForPreprocess(schemaObj, this._schemaMap);
1292
- // The mutator pass (defaults, coercion, removal) is generated source too,
1293
- // with the schema's `default` values embedded; an interpreter-only
1294
- // validator takes the closure mutators instead.
1295
- let preprocess = this._interpretOnly ? null : buildPreprocessCodegen(preprocessSchema, options);
1296
- if (!preprocess) {
1297
- const applyDefaults = options.useDefaults === false ? null : buildDefaultsApplier(preprocessSchema);
1298
- const applyCoerce = options.coerceTypes ? buildCoercer(preprocessSchema) : null;
1299
- const applyRemove = options.removeAdditional
1300
- ? buildRemover(preprocessSchema)
1301
- : null;
1302
- const mutators = [applyRemove, applyCoerce, applyDefaults].filter(Boolean);
1303
- preprocess =
1304
- mutators.length === 0
1305
- ? null
1306
- : mutators.length === 1
1307
- ? mutators[0]
1308
- : (data) => {
1309
- for (let i = 0; i < mutators.length; i++) mutators[i](data);
1310
- };
1311
- }
1312
- this._applyDefaults = preprocess;
1313
- // Whether validate() can change the caller's object before the verdict.
1314
- // This is a capability, not an option: `useDefaults` is on by default, but
1315
- // buildDefaultsApplier returns null when the schema declares no defaults,
1316
- // so a plain schema is genuinely non-mutating. The renderers refuse to
1317
- // synthesize a frame when this is true, because a frame built from mutated
1318
- // data would show the reader a value they never sent.
1319
- this._mutatesInput = !!(preprocess || options.coerceTypes || options.removeAdditional);
1320
- this._preprocess = preprocess;
1321
-
1322
- // removeAdditional alone, the common parse-and-strip use: a verdict function
1323
- // that deletes unknown keys in the walk it already makes, where the pass
1324
- // above walks every object a second time just to find them. It answers
1325
- // the documents it accepts; anything it rejects takes the full path below,
1326
- // which removes, validates and reports exactly as before, so a rejected
1327
- // document is left as clean as it always was. The generator declines any
1328
- // schema where deleting during the walk could change an answer.
1329
- let fusedRemove = null;
1330
- if (preprocess && options.removeAdditional && !options.coerceTypes && !this._interpretOnly &&
1331
- !this._userFormats && !this._usesKeywords && !this._schemaStr.includes('"default"')) {
1332
- try {
1333
- fusedRemove = compileToJSCodegen(schemaObj, this._schemaMap.size > 0 ? this._schemaMap : null, null, { removeAdditional: true });
1334
- } catch {
1335
- fusedRemove = null;
1336
- }
1337
- }
1338
-
1339
- // Detect if schema is "selective" -- doesn't recurse into arrays/deep objects.
1340
- const hasArrayTraversal =
1341
- schemaObj &&
1342
- (schemaObj.items ||
1343
- schemaObj.prefixItems ||
1344
- schemaObj.contains ||
1345
- (schemaObj.properties &&
1346
- Object.values(schemaObj.properties).some(
1347
- (p) => p && (p.items || p.prefixItems || p.contains),
1348
- )));
1349
- const useSimdjsonForLarge = !hasArrayTraversal;
1350
-
1351
- // Build the generators the compile step left out, each only when something
1352
- // asks for it, and upgrade the shared cache entry. A first rejection needs
1353
- // one of the two, not both; building both was a millisecond of V8 compiling
1354
- // a generator nobody called. `undefined` means not built yet; `null` means
1355
- // the compiler declined. Conflating those is what once cost every schema
1356
- // its generated error function for the life of the process, so they stay
1357
- // apart, and `full` is set only once both exist.
1358
- const _upgradeCacheEntry = () => {
1359
- if (this._userFormats) return;
1360
- const entry = _compileCache.get(mapKey);
1361
- if (entry && entry.jsFn === jsFn) {
1362
- if (jsCombinedFn !== undefined) entry.combined = jsCombinedFn;
1363
- if (jsErrFn !== undefined) entry.errFn = jsErrFn;
1364
- if (jsCombinedFn !== undefined && jsErrFn !== undefined) entry.full = true;
1365
- }
1366
- };
1367
- const _buildCombined = () => {
1368
- if (jsCombinedFn !== undefined) return;
1369
- jsCombinedFn = compileToJSCombined(schemaObj, VALID_RESULT, sm, this._userFormats) || null;
1370
- _upgradeCacheEntry();
1371
- };
1372
- const _buildErr = () => {
1373
- if (jsErrFn !== undefined) return;
1374
- jsErrFn = compileToJSCodegenWithErrors(schemaObj, sm, this._userFormats) || null;
1375
- _upgradeCacheEntry();
1376
- };
1377
-
1378
- if (jsFn) {
1379
- // errFn: use JS codegen if safe, else native fallback (only when native
1380
- // is available). Environments without the native addon — Cloudflare
1381
- // Workers, browser, Bun without N-API — get a JS-only fallback so the
1382
- // invalid path doesn't dereference a null _compiled.
1383
- const hasUnevaluated = schemaObj && (schemaObj.unevaluatedProperties !== undefined || schemaObj.unevaluatedItems !== undefined || this._schemaStr.includes('unevaluatedProperties') || this._schemaStr.includes('unevaluatedItems'))
1384
- const hasDynRef = this._schemaStr.includes('"$dynamicRef"') || this._schemaStr.includes('"$dynamicAnchor"')
1385
- // Native-less error path: the interpreted engine re-validates failing
1386
- // data to produce full errors. If it disagrees with the codegen verdict
1387
- // (it should not), a generic error keeps the result consistent.
1388
- let _interp = null;
1389
- const jsOnlyFallback = (d) => {
1390
- if (jsFn(d)) return { valid: true, data: d, errors: [] };
1391
- if (!_interp) {
1392
- const { createInterpreter } = require('./lib/interpreter');
1393
- _interp = createInterpreter(schemaObj, {
1394
- schemaMap: this._schemaMap.size > 0 ? this._schemaMap : null,
1395
- formats: this._userFormats,
1396
- v1: isV1Dialect(schemaObj),
1397
- keywords: this._keywords,
1398
- });
1399
- }
1400
- const r = _interp.validate(d);
1401
- if (!r.valid) return r;
1402
- return {
1403
- valid: false,
1404
- errors: [{
1405
- keyword: 'validation',
1406
- instancePath: '',
1407
- schemaPath: '',
1408
- params: {},
1409
- message: 'schema validation failed'
1410
- }]
1411
- };
1412
- };
1413
- // The error generator declines unevaluated*; the interpreted engine
1414
- // reports those schemas correctly, so failing data is re-validated
1415
- // there. This used to be a placeholder error with no keyword and no
1416
- // path, which hid whatever had actually failed.
1417
- // Resolved on the first rejection rather than at compile time, because
1418
- // building the generator behind it is two thirds of what a first call
1419
- // costs and a caller that never reads an error never needs it. The probe
1420
- // moves here with it: it calls the generated function, so it cannot run
1421
- // before the function exists.
1422
- let _errOnlyImpl = null;
1423
- const errOnly = (d) => {
1424
- if (_errOnlyImpl === null) {
1425
- _buildErr();
1426
- let safe = null;
1427
- if (jsErrFn) {
1428
- try {
1429
- jsErrFn({}, true);
1430
- safe = (x) => jsErrFn(x, true);
1431
- } catch {}
1432
- }
1433
- _errOnlyImpl =
1434
- safe ||
1435
- (hasUnevaluated || !native
1436
- ? jsOnlyFallback
1437
- : hasDynRef
1438
- ? (x) => {
1439
- this._ensureNative();
1440
- return this._compiled.validateJSON(JSON.stringify(x));
1441
- }
1442
- : (x) => {
1443
- this._ensureNative();
1444
- return this._compiled.validate(x);
1445
- });
1446
- }
1447
- return _mustReject(_errOnlyImpl(d));
1448
- };
1449
-
1450
- // Best path: combined validator (single pass, validates + collects errors)
1451
- // Valid data: returns VALID_RESULT, no allocation
1452
- // Invalid data: collects errors in one pass (no double validation)
1453
- // Fallback: hybridFn or jsFn + errFn for schemas combined can't handle
1454
- // Test combined at compile time -- some schemas produce broken combined code
1455
- // Test combined at compile time -- some schemas (e.g. if/then/else)
1456
- // produce broken combined code that crashes on certain inputs.
1457
- // We probe with diverse data; if any throws, fall back to hybrid.
1458
- let _combinedProbed = false;
1459
- let _safeCombined = null;
1460
- const combinedIfSafe = () => {
1461
- if (_combinedProbed) return _safeCombined;
1462
- _combinedProbed = true;
1463
- _buildCombined();
1464
- if (jsCombinedFn) {
1465
- try {
1466
- const probe = {};
1467
- // Populate probe with one key per known property to trigger nested paths
1468
- if (schemaObj && schemaObj.properties) {
1469
- for (const k of Object.keys(schemaObj.properties)) probe[k] = "";
1470
- }
1471
- if (schemaObj && schemaObj.if && schemaObj.if.properties) {
1472
- for (const k of Object.keys(schemaObj.if.properties)) probe[k] = "";
1473
- }
1474
- jsCombinedFn(probe);
1475
- jsCombinedFn({});
1476
- jsCombinedFn(null);
1477
- jsCombinedFn(0);
1478
- _safeCombined = jsCombinedFn;
1479
- } catch {}
1480
- }
1481
- return _safeCombined;
1482
- };
1483
-
1484
- // What the hybrid path hands to its error slot: the combined function
1485
- // when it is usable, since it validates and collects in one pass, and
1486
- // the error generator otherwise. Same order the eager code chose, just
1487
- // chosen on the first rejection.
1488
- let _errPreferredImpl = null;
1489
- const errPreferCombined = (d) => {
1490
- if (_errPreferredImpl === null) _errPreferredImpl = combinedIfSafe() || errOnly;
1491
- return _mustReject(_errPreferredImpl(d));
1492
- };
1493
-
1494
- // The boolean engine is the verdict authority for these paths; the
1495
- // final lazy wrapper uses it to skip error construction entirely.
1496
- if (!hasDynRef || _isCodegen) this._fastVerdict = preprocess ? null : jsFn;
1497
-
1498
- if (options.abortEarly && jsFn && !hasDynRef) {
1499
- // abortEarly: do NOT enrich. Skip position lookups, suggestions, source maps.
1500
- // This is the perf-critical path for edge gateways. The richErrors wrap
1501
- // below recognises the ATA9000 stub keyword and passes the frozen result
1502
- // through unchanged, so a single shared object is returned per failure.
1503
- const _fn = jsFn;
1504
- this.validate = preprocess
1505
- ? (data) => { preprocess(data); return _fn(data) ? VALID_RESULT : ABORT_EARLY_RESULT; }
1506
- : (data) => (_fn(data) ? VALID_RESULT : ABORT_EARLY_RESULT);
1507
- } else if (hasDynRef && _isCodegen && jsFn) {
1508
- // $dynamicRef with JS codegen: direct path, no wrapper layers
1509
- const _fn = jsFn, _efn = errOnly, _R = VALID_RESULT;
1510
- this.validate = preprocess
1511
- ? (data) => { preprocess(data); return _fn(data) ? _R : _efn(data); }
1512
- : (data) => _fn(data) ? _R : _efn(data);
1513
- } else if (hasDynRef) {
1514
- // $dynamicRef without codegen: the interpreted engine. It scores the
1515
- // same on the suite's $dynamicRef cases as the native walker since the
1516
- // dynamic-scope fix, needs no addon, and gets the verdict-only mode.
1517
- if (!_interp) {
1518
- const { createInterpreter } = require('./lib/interpreter');
1519
- _interp = createInterpreter(schemaObj, {
1520
- schemaMap: this._schemaMap.size > 0 ? this._schemaMap : null,
1521
- formats: this._userFormats,
1522
- v1: isV1Dialect(schemaObj),
1523
- keywords: this._keywords,
1524
- });
1525
- }
1526
- const interp = _interp;
1527
- this._fastVerdict = preprocess ? null : (d) => interp.isValid(d);
1528
- this.validate = preprocess
1529
- ? (data) => { preprocess(data); return interp.validate(data); }
1530
- : (data) => interp.validate(data);
1531
- } else if (jsFn && jsFn._hybridFactory) {
1532
- // Zero-wrapper: hybridFactory bakes VALID_RESULT + errFn into a single function
1533
- // No arrow function wrapper, no ternary, one function call
1534
- // The factory bakes the error function in as an argument and never
1535
- // calls it for a document that passes, so a resolver here costs the
1536
- // accepted path nothing and keeps the compile off the first call.
1537
- // Until the first rejection this is the hybrid: the verdict function,
1538
- // with the error resolver baked in and never called for a document that
1539
- // passes. That keeps the combined function's compile off the first call,
1540
- // which is two thirds of what a first call costs.
1541
- //
1542
- // From the first rejection on, the combined function answers directly.
1543
- // It decides and collects in one pass, so the verdict pass in front of it
1544
- // was validating the document a second time: 134.1 microseconds against
1545
- // its 45.3 on a 1000-user array. Swapping rather than starting there keeps
1546
- // the lazy compile, and swapping at all is only free because the combined
1547
- // function now costs what the verdict function costs on accepted
1548
- // documents (1.02x on that array, 0.99x on a small body) since
1549
- // additionalProperties stopped materialising its key array. The
1550
- // indirection this needs measured inside the noise at both sizes.
1551
- let impl = null;
1552
- const onReject = (data) => {
1553
- const combined = combinedIfSafe();
1554
- if (combined) { impl = combined; return _mustReject(combined(data)); }
1555
- return errOnly(data);
1556
- };
1557
- // Tiered: the first calls go through the verdict function and the
1558
- // resolver, and the hybrid is compiled once the validator is in use.
1559
- // Compiling it up front was a second parse of the whole schema on
1560
- // every validator, most of which answer a handful of requests or only
1561
- // verdicts. A refused compile stays on the first tier.
1562
- let warm = 0;
1563
- const first = (data) => {
1564
- if (++warm >= HYBRID_TIER_CALLS && impl === first) {
1565
- impl = jsFn._hybridFactory(VALID_RESULT, onReject) || ((d) => (jsFn(d) ? VALID_RESULT : onReject(d)));
1566
- return impl(data);
1567
- }
1568
- return jsFn(data) ? VALID_RESULT : onReject(data);
1569
- };
1570
- impl = first;
1571
- const run = (data) => impl(data);
1572
- this.validate = preprocess
1573
- ? (data) => { preprocess(data); return run(data); }
1574
- : run;
1575
- } else {
1576
- // No hybrid factory, so the assembly needs the function itself rather
1577
- // than a reference it can call later: build it now.
1578
- const safeCombinedFn = combinedIfSafe();
1579
- if (safeCombinedFn) {
1580
- this.validate = preprocess
1581
- ? (data) => { preprocess(data); return safeCombinedFn(data); }
1582
- : safeCombinedFn;
1583
- } else {
1584
- this.validate = preprocess
1585
- ? (data) => {
1586
- preprocess(data);
1587
- return jsFn(data) ? VALID_RESULT : errOnly(data);
1588
- }
1589
- : (data) => (jsFn(data) ? VALID_RESULT : errOnly(data));
1590
- }
1591
- }
1592
- // Verbose mode: populate parentSchema, schema and data on each error, the
1593
- // three fields the default error shape carries under the same option.
1594
- // `data` is the value the error points at; without it a caller has to
1595
- // walk the document by the instance path itself, which is what one
1596
- // migration ended up writing by hand. Errors may be frozen, so clone
1597
- // them with the extra fields.
1598
- if (this._verbose) {
1599
- const inner = this.validate;
1600
- const root = this._schemaObj;
1601
- const { resolvePointer } = require('./lib/pointer.js');
1602
- this.validate = (data) => {
1603
- const result = inner(data);
1604
- if (result && !result.valid && result.errors) {
1605
- const enriched = result.errors.map((err) => {
1606
- if (!err || err.parentSchema !== undefined) return err;
1607
- const parentSchema = resolveSchemaByPath(root, err.schemaPath);
1608
- // The last segment of the schema path is the keyword that
1609
- // failed, so its value on the parent is that keyword's schema.
1610
- const sp = typeof err.schemaPath === 'string' ? err.schemaPath : '';
1611
- const last = sp.slice(sp.lastIndexOf('/') + 1).replace(/~1/g, '/').replace(/~0/g, '~');
1612
- const keywordSchema = (parentSchema !== null && typeof parentSchema === 'object' && last)
1613
- ? parentSchema[last]
1614
- : undefined;
1615
- return {
1616
- ...err,
1617
- parentSchema,
1618
- schema: keywordSchema,
1619
- data: resolvePointer(data, err.instancePath, undefined),
1620
- };
1621
- });
1622
- return { valid: false, errors: enriched };
1623
- }
1624
- return result;
1625
- };
1626
- }
1627
- // The verdict methods answer validate()'s question without building the
1628
- // error list, so they run the same preprocess pass. Skipping it made the
1629
- // two disagree on input that coercion or a default would have fixed.
1630
- _bindVerdict(this, fusedRemove
1631
- ? (data) => fusedRemove(data) || (preprocess(data), jsFn(data))
1632
- : preprocess
1633
- ? (data) => { preprocess(data); return jsFn(data) }
1634
- : jsFn);
1635
- // Same preference as the object path: the combined function first, since
1636
- // it validates and collects in one pass, and the error generator behind
1637
- // it. `errPreferCombined` is that order, resolved on the first rejection
1638
- // instead of at compile time.
1639
- // Tiered the same way as validate() above.
1640
- let jsonHybrid = null;
1641
- let jsonWarm = 0;
1642
- const jsonValidateInner = (obj) => {
1643
- if (jsonHybrid !== null) return jsonHybrid(obj);
1644
- if (++jsonWarm >= HYBRID_TIER_CALLS) {
1645
- jsonHybrid = (jsFn._hybridFactory && jsFn._hybridFactory(VALID_RESULT, errPreferCombined))
1646
- || ((o) => (jsFn(o) ? VALID_RESULT : errPreferCombined(o)));
1647
- return jsonHybrid(obj);
1648
- }
1649
- return jsFn(obj) ? VALID_RESULT : errPreferCombined(obj);
1650
- };
1651
- // Parsed text takes the same preprocess pass as a parsed object, so
1652
- // validate(obj) and validateJSON(text) answer the same for the same
1653
- // document. Without it, coercion, removal and defaults applied on one
1654
- // path and not the other.
1655
- const jsonValidateFn = preprocess
1656
- ? (obj) => { preprocess(obj); return jsonValidateInner(obj) }
1657
- : jsonValidateInner;
1658
- this.validateJSON = useSimdjsonForLarge && native && !preprocess
1659
- ? (jsonStr) => {
1660
- // `_skipNativeFast` is set by the scanner short-circuit below when it
1661
- // has already decided the document is invalid. The encode and the
1662
- // native call would run only to return false, and the error path
1663
- // underneath does not need them.
1664
- if (jsonStr.length >= SIMDJSON_THRESHOLD && this._skipNativeFast !== true) {
1665
- this._ensureNative();
1666
- const buf = Buffer.from(jsonStr);
1667
- if (native.rawFastValidate(this._fastSlot, buf))
1668
- return VALID_RESULT;
1669
- return this._compiled.validateJSON(jsonStr);
1670
- }
1671
- try {
1672
- return jsonValidateFn(JSON.parse(jsonStr));
1673
- } catch (e) {
1674
- if (!(e instanceof SyntaxError)) throw e;
1675
- }
1676
- this._ensureNative();
1677
- return this._compiled.validateJSON(jsonStr);
1678
- }
1679
- : (jsonStr) => {
1680
- try {
1681
- return jsonValidateFn(JSON.parse(jsonStr));
1682
- } catch (e) {
1683
- if (!(e instanceof SyntaxError)) throw e;
1684
- if (!native) return { valid: false, errors: [{ keyword: 'syntax', instancePath: '', schemaPath: '#', params: {}, message: e.message }] };
1685
- }
1686
- this._ensureNative();
1687
- return this._compiled.validateJSON(jsonStr);
1688
- };
1689
- // The addon validates the bytes as they are, which is the wrong answer
1690
- // when the schema asks for coercion, removal or defaults: those change
1691
- // what counts as valid. With a preprocess pass configured the text is
1692
- // parsed and run through the same path validate() takes.
1693
- const verdictFromText = (jsonStr) => {
1694
- let parsed;
1695
- try {
1696
- parsed = JSON.parse(jsonStr);
1697
- } catch (e) {
1698
- if (!(e instanceof SyntaxError)) throw e;
1699
- return false;
1700
- }
1701
- if (preprocess) preprocess(parsed);
1702
- return jsFn(parsed);
1703
- };
1704
- this.isValidJSON = useSimdjsonForLarge && native && !preprocess
1705
- ? (jsonStr) => {
1706
- if (jsonStr.length >= SIMDJSON_THRESHOLD) {
1707
- this._ensureNative();
1708
- return native.rawFastValidate(
1709
- this._fastSlot,
1710
- Buffer.from(jsonStr),
1711
- );
1712
- }
1713
- return verdictFromText(jsonStr);
1714
- }
1715
- : verdictFromText;
1716
-
1717
- // A schema-directed scanner answers the verdict from the JSON text
1718
- // without building the document. Parsing is around three quarters of the
1719
- // cost of a real request, and a caller that only wants yes or no should
1720
- // not pay it; a rejection can also stop at the byte that caused it
1721
- // instead of parsing the rest of a document that is already refused.
1722
- //
1723
- // It is wired only where the verdict IS the answer. On a path that has
1724
- // to produce errors, scanning an invalid document is work thrown away,
1725
- // so those keep parsing. `abortEarly` has no errors to produce, so it
1726
- // counts as a verdict path.
1727
- //
1728
- // Not wired when a preprocess pass is configured: coercion, removal and
1729
- // defaults rewrite the document before it is judged, and the scanner
1730
- // reads what arrived. The compiler declines any schema it cannot answer
1731
- // and a compiled scanner returns BAIL for a document shape it cannot
1732
- // answer, and then the parse path below takes over unchanged.
1733
- // validateAndParse: parse the JSON, then validate. Pure JS (JSON.parse +
1734
- // validate) so it works with or without the native addon and in browsers.
1735
- {
1736
- const self = this;
1737
- this.validateAndParse = (jsonStr) => {
1738
- let value;
1739
- try {
1740
- value = JSON.parse(typeof jsonStr === 'string' ? jsonStr : new TextDecoder().decode(jsonStr));
1741
- } catch (e) {
1742
- return { valid: false, value: undefined, errors: [{ code: 'ATA9001', message: 'invalid JSON: ' + e.message, keyword: '__parse__', instancePath: '', schemaPath: '', params: {} }] };
1743
- }
1744
- const r = self.validate(value);
1745
- return { valid: r.valid, value, errors: r.errors };
1746
- };
1747
- }
1748
- // Buffer APIs: lazy native init — only compile native schema on first buffer call.
1749
- // This keeps cold start fast (JS codegen only) for users who only use validate().
1750
- if (native) {
1751
- const self = this;
1752
- this.isValid = (buf) => {
1753
- self._ensureNative();
1754
- const slot = self._fastSlot;
1755
- self.isValid = slot < 0
1756
- ? (b) => self._slowBufferValid(b, 'isValid')
1757
- : (b) => {
1758
- if (typeof b === 'string') b = Buffer.from(b);
1759
- else if (!(b instanceof Uint8Array)) throw new TypeError('isValid() requires a Buffer, Uint8Array, or string. For parsed objects, use isValidObject().');
1760
- return native.rawFastValidate(slot, b);
1761
- };
1762
- return self.isValid(buf);
1763
- };
1764
- this.countValid = (ndjsonBuf) => {
1765
- self._ensureNative();
1766
- const slot = self._fastSlot;
1767
- self.countValid = slot < 0
1768
- ? (b) => {
1769
- if (typeof b !== 'string' && !(b instanceof Uint8Array)) throw new TypeError('countValid() requires a Buffer, Uint8Array, or string');
1770
- const text = typeof b === 'string' ? b : Buffer.from(b.buffer, b.byteOffset, b.byteLength).toString('utf8');
1771
- let c = 0;
1772
- for (const line of text.split('\n')) {
1773
- if (line.trim() === '') continue;
1774
- if (self._slowBufferValid(line, 'countValid')) c++;
1775
- }
1776
- return c;
1777
- }
1778
- : (b) => {
1779
- if (typeof b === 'string') b = Buffer.from(b);
1780
- else if (!(b instanceof Uint8Array)) throw new TypeError('countValid() requires a Buffer, Uint8Array, or string');
1781
- const r = native.rawNDJSONValidate(slot, b);
1782
- let c = 0;
1783
- for (let i = 0; i < r.length; i++) if (r[i]) c++;
1784
- return c;
1785
- };
1786
- return self.countValid(ndjsonBuf);
1787
- };
1788
- this.batchIsValid = (buffers) => {
1789
- self._ensureNative();
1790
- const slot = self._fastSlot;
1791
- self.batchIsValid = slot < 0
1792
- ? (bufs) => {
1793
- let v = 0;
1794
- for (const b of bufs) {
1795
- if (!(b instanceof Uint8Array)) throw new TypeError('batchIsValid() requires Buffer or Uint8Array elements');
1796
- if (self._slowBufferValid(b, 'batchIsValid')) v++;
1797
- }
1798
- return v;
1799
- }
1800
- : (bufs) => {
1801
- let v = 0;
1802
- for (const b of bufs) {
1803
- if (!(b instanceof Uint8Array)) throw new TypeError('batchIsValid() requires Buffer or Uint8Array elements');
1804
- if (native.rawFastValidate(slot, b)) v++;
1805
- }
1806
- return v;
1807
- };
1808
- return self.batchIsValid(buffers);
1809
- };
1810
- }
1811
- } else if (native) {
1812
- // No JS codegen: buffer/parallel APIs always come from the native
1813
- // engine, but the object-validation entry points go to whichever
1814
- // engine is more correct for the schema shape. Pure dynamic-ref
1815
- // schemas stay on the C++ path (full $dynamicRef scope tracking);
1816
- // everything else uses the interpreted engine, which handles the
1817
- // $id/URN base-URI resolution corners the native resolver gets wrong.
1818
- this._ensureNative();
1819
- const _hasDynRef = this._schemaStr.includes('"$dynamicRef"') || this._schemaStr.includes('"$dynamicAnchor"')
1820
- const _hasUneval = this._schemaStr.includes('"unevaluatedProperties"') || this._schemaStr.includes('"unevaluatedItems"')
1821
- // propertyDependencies exists only in the interpreted engine, so a schema
1822
- // using it goes there even when it also uses $dynamicRef.
1823
- const _hasPropDeps = this._schemaStr.includes('"propertyDependencies"')
1824
- // $dynamicRef used to delegate to the native validateJSON path here;
1825
- // the interpreted engine now scores the same on those cases, carries
1826
- // the verdict-only mode, and works without the addon.
1827
- let _validate;
1828
- {
1829
- const { createInterpreter } = require('./lib/interpreter');
1830
- const interp = createInterpreter(schemaObj, {
1831
- schemaMap: this._schemaMap.size > 0 ? this._schemaMap : null,
1832
- formats: this._userFormats,
1833
- v1: isV1Dialect(schemaObj),
1834
- keywords: this._keywords,
1835
- });
1836
- this._engine = 'interpreter';
1837
- _validate = (data) => interp.validate(data);
1838
- this._fastVerdict = preprocess ? null : (d) => interp.isValid(d);
1839
- this.validateJSON = (jsonStr) => {
1840
- try {
1841
- return _validate(JSON.parse(jsonStr));
1842
- } catch (e) {
1843
- return { valid: false, errors: [{ keyword: 'syntax', instancePath: '', schemaPath: '#', params: {}, message: e.message }] };
1844
- }
1845
- };
1846
- this.isValidJSON = (jsonStr) => this.validateJSON(jsonStr).valid;
1847
- }
1848
- this.validate = preprocess
1849
- ? (data) => {
1850
- preprocess(data);
1851
- return _validate(data);
1852
- }
1853
- : _validate;
1854
- _bindVerdict(this, this._fastVerdict
1855
- ? this._fastVerdict
1856
- // The rewrite runs first here too, as it does for validate() above; a
1857
- // verdict without it disagreed with validate() on input that coercion,
1858
- // a default or a removed key would have fixed.
1859
- : preprocess
1860
- ? (data) => { preprocess(data); return _validate(data).valid; }
1861
- : (data) => _validate(data).valid);
1862
- this.validateAndParse = (jsonStr) => this._compiled.validateAndParse(jsonStr);
1863
- {
1864
- const slot = this._fastSlot;
1865
- this.isValid = (buf) => {
1866
- if (typeof buf === 'string') buf = Buffer.from(buf);
1867
- else if (!(buf instanceof Uint8Array)) throw new TypeError('isValid() requires a Buffer, Uint8Array, or string. For parsed objects, use isValidObject().');
1868
- return native.rawFastValidate(slot, buf);
1869
- };
1870
- }
1871
- {
1872
- const slot = this._fastSlot;
1873
- this.countValid = (ndjsonBuf) => {
1874
- if (typeof ndjsonBuf === 'string') ndjsonBuf = Buffer.from(ndjsonBuf);
1875
- else if (!(ndjsonBuf instanceof Uint8Array)) throw new TypeError('countValid() requires a Buffer, Uint8Array, or string');
1876
- const results = native.rawNDJSONValidate(slot, ndjsonBuf);
1877
- let count = 0;
1878
- for (let i = 0; i < results.length; i++) if (results[i]) count++;
1879
- return count;
1880
- };
1881
- }
1882
- {
1883
- const slot = this._fastSlot;
1884
- this.batchIsValid = (buffers) => {
1885
- let valid = 0;
1886
- for (const buf of buffers) {
1887
- if (!(buf instanceof Uint8Array)) throw new TypeError('batchIsValid() requires Buffer or Uint8Array elements');
1888
- if (native.rawFastValidate(slot, buf)) valid++;
1889
- }
1890
- return valid;
1891
- };
1892
- }
1893
- } else {
1894
- // No JS codegen and no native engine: fall back to the interpreted
1895
- // engine. Slow but correct, and strictly better than the previous
1896
- // behavior (the lazy stubs re-dispatched to themselves forever).
1897
- const { createInterpreter } = require('./lib/interpreter');
1898
- const interp = createInterpreter(schemaObj, {
1899
- schemaMap: this._schemaMap.size > 0 ? this._schemaMap : null,
1900
- formats: this._userFormats,
1901
- v1: isV1Dialect(schemaObj),
1902
- keywords: this._keywords,
1903
- });
1904
- this._engine = 'interpreter';
1905
- if (!preprocess) this._fastVerdict = (d) => interp.isValid(d);
1906
- // abortEarly is a documented contract, not a property of whichever engine
1907
- // answered: it promises the frozen ATA9000 stub instead of a detailed
1908
- // error, so code that branches on it has to behave the same with and
1909
- // without code generation. Taking the verdict path here also skips
1910
- // building the errors the caller said it did not want.
1911
- const run = options.abortEarly
1912
- ? (preprocess
1913
- ? (data) => { preprocess(data); return interp.isValid(data) ? VALID_RESULT : ABORT_EARLY_RESULT; }
1914
- : (data) => (interp.isValid(data) ? VALID_RESULT : ABORT_EARLY_RESULT))
1915
- : (preprocess
1916
- ? (data) => { preprocess(data); return interp.validate(data); }
1917
- : (data) => interp.validate(data));
1918
- this.validate = run;
1919
- _bindVerdict(this, this._fastVerdict
1920
- ? this._fastVerdict
1921
- : (data) => run(data).valid);
1922
- this.validateJSON = (jsonStr) => {
1923
- try {
1924
- return run(JSON.parse(jsonStr));
1925
- } catch (e) {
1926
- return { valid: false, errors: [{ keyword: 'syntax', instancePath: '', schemaPath: '#', params: {}, message: e.message }] };
1927
- }
1928
- };
1929
- this.isValidJSON = (jsonStr) => this.validateJSON(jsonStr).valid;
1930
- }
1931
-
1932
- // Error presentation, one lazy layer: declaration-order sorting, rich
1933
- // enrichment (received value, suggestions, source frames, docUrl), or the
1934
- // raw v0.14 shape under `richErrors: false`. All of it is work a caller
1935
- // that only reads `.valid` never sees, so it runs on first access to
1936
- // `.errors` and is cached. One wrapper, one allocation per rejection.
1937
- if (this.validate) {
1938
- const inner = this.validate;
1939
- const enrich = this._richErrors ? require('./lib/enrich-error').enrich : null;
1940
- const root = this._schemaObj;
1941
- const self = this;
1942
- this.validate = (data) => {
1943
- const result = inner(data);
1944
- // abortEarly returns the shared ATA9000 stub; preserve it as-is so the
1945
- // perf fast path stays allocation-free and the documented code stays stable.
1946
- if (result && result.valid === false && result !== ABORT_EARLY_RESULT) {
1947
- // The raw input travels with the rejection when validateJSON set one.
1948
- // The map it implies is built on first access to `.errors`, so a
1949
- // caller reading only `.valid` does not pay for a document walk.
1950
- const rawInput = enrich ? self._lastRawInput : null;
1951
- // One instance of a class with prototype accessors. An object
1952
- // literal with a getter here cost a closure plus an accessor
1953
- // definition on every rejection, several hundred nanoseconds
1954
- // before any error was read.
1955
- return new RichRejection(result, data, rawInput, self, root, enrich);
1956
- }
1957
- return result;
1958
- };
1959
-
1960
- // validateJSON also enriches: set _lastRawInput so the position cache
1961
- // can lazily build a map for dataFrame attachment. Only validateJSON
1962
- // wires this — validate(data) takes a pre-parsed object, by design.
1963
- if (this._richErrors && this.validateJSON) {
1964
- const innerJson = this.validateJSON;
1965
- this.validateJSON = (jsonStr) => {
1966
- // The inner path reads _lastRawInput to hand the raw text to the
1967
- // rejection it builds. Cleared as soon as it returns: the rejection
1968
- // carries the text itself, so nothing outlives the call.
1969
- this._lastRawInput = jsonStr;
1970
- let result;
1971
- try {
1972
- result = innerJson(jsonStr);
1973
- } finally {
1974
- this._lastRawInput = null;
1975
- }
1976
- // Every diagnostic the text path adds is deferred. Deciding here
1977
- // whether there is anything to add would mean reading `result.errors`,
1978
- // and on the codegen path that realizes the inner lazy layer, which is
1979
- // the document walk this exists to avoid.
1980
- if (result && result.valid === false) {
1981
- return new LazyJsonRejection(result, jsonStr, this, enrich);
1982
- }
1983
- return result;
1984
- };
1985
- }
1986
- }
1987
-
1988
- // validate() resolves a typed `data` on success: the validated input, after
1989
- // any in-place coercion/defaults. This matches the ValidationResult<T>
1990
- // contract. isValidObject() and abortEarly stay allocation-free for hot
1991
- // paths that only need a boolean.
1992
- if (this.validate) {
1993
- const _bare = this.validate;
1994
- this.validate = fusedRemove
1995
- // A document the removing verdict accepts is answered here, one call
1996
- // deep; anything else takes the full path, see fusedRemove above.
1997
- ? (data) => {
1998
- if (fusedRemove(data)) return { valid: true, data, errors: VALID_RESULT.errors };
1999
- const r = _bare(data);
2000
- return (r.valid === true && r.data === undefined)
2001
- ? { valid: true, data, errors: r.errors }
2002
- : r;
2003
- }
2004
- : (data) => {
2005
- const r = _bare(data);
2006
- return (r.valid === true && r.data === undefined)
2007
- ? { valid: true, data, errors: r.errors }
2008
- : r;
2009
- };
2010
- }
2011
-
2012
- // Custom error messages: if any subschema declares an `errorMessage`
2013
- // keyword, install an outermost decorator that overrides the `message`
2014
- // field of the errors it owns. Gated on a one-time scan so schemas without
2015
- // errorMessage keep the validate hot path untouched. Layered after rich
2016
- // enrichment so `code`/`keyword`/`path` are already final and only the
2017
- // human-facing message changes.
2018
- {
2019
- const emLib = require('./lib/error-messages');
2020
- const schemaStr = this._schemaStr || (this._schemaObj ? JSON.stringify(this._schemaObj) : '');
2021
- if (emLib.schemaHasErrorMessages(schemaStr)) {
2022
- const root = this._schemaObj;
2023
- const wrap = (inner) => (arg) => {
2024
- const result = inner(arg);
2025
- if (result && result.valid === false && result.errors && result.errors.length && result !== ABORT_EARLY_RESULT) {
2026
- const overridden = emLib.applyErrorMessages(result.errors, root);
2027
- if (overridden !== result.errors) return { valid: false, errors: overridden };
2028
- }
2029
- return result;
2030
- };
2031
- if (this.validate) this.validate = wrap(this.validate);
2032
- if (this.validateJSON) this.validateJSON = wrap(this.validateJSON);
2033
- // validateAndParse routes through self.validate on the codegen path, but
2034
- // the native-only path returns directly from the addon — wrap it so both
2035
- // paths get overrides. The result shape carries `value`, preserved here.
2036
- if (this.validateAndParse) {
2037
- const innerVP = this.validateAndParse;
2038
- this.validateAndParse = (arg) => {
2039
- const result = innerVP(arg);
2040
- if (result && result.valid === false && result.errors && result.errors.length) {
2041
- const overridden = emLib.applyErrorMessages(result.errors, root);
2042
- if (overridden !== result.errors) return { valid: false, value: result.value, errors: overridden };
2043
- }
2044
- return result;
2045
- };
2046
- }
2047
- }
2048
- }
2049
-
2050
- // Errors are paid for when read, not when produced. The full pipeline
2051
- // above (error codegen, enrichment, custom messages, verbose) stays
2052
- // intact, but validate() now answers the verdict from the boolean
2053
- // engine and materializes `errors` through a getter on first access.
2054
- // A caller that only reads `.valid`, which is every gateway check and
2055
- // every benchmark, skips error construction entirely; a caller that
2056
- // reads `.errors` pays once and the result is cached. Skipped when the
2057
- // schema coerces or defaults (preprocess mutates before the verdict),
2058
- // under abortEarly (already a frozen stub), and for $dynamicRef (the
2059
- // boolean engine is not the authority there).
2060
- // A check registered through _extendValidate joins here when it can: the
2061
- // verdict comes from the generated function with the check compiled in,
2062
- // and the check's own errors are appended when somebody reads them. A
2063
- // rejection then costs what it costs without the check. Where this layer
2064
- // is not installed, the end of this method wraps validate() instead.
2065
- const _vx = this._validateTail !== null ? this._validateTail() : null;
2066
- let _vxApplied = false;
2067
- if (this._fastVerdict && !preprocess && !options.abortEarly && this.validate) {
2068
- const _full = this.validate;
2069
- const _fast = _vx ? _fuseTail(this._fastVerdict, _vx.check) : this._fastVerdict;
2070
- const _extra = _vx ? _vx.errors : null;
2071
- _vxApplied = true;
2072
- const EMPTY_ERRORS = Object.freeze([]);
2073
- const _verdictFallback = [{ keyword: 'validation', instancePath: '', schemaPath: '#', params: {}, message: 'schema validation failed' }];
2074
- const _withExtra = (own, data) => {
2075
- const more = _extra === null ? null : _extra(data);
2076
- if (more && more.length) return own ? own.concat(more) : more;
2077
- // The data changed between the verdict and this read; keep the
2078
- // verdict and say so rather than inventing a specific error.
2079
- return own || _verdictFallback;
2080
- };
2081
- const _buildErrors = (data) => {
2082
- const r = _full(data);
2083
- return _withExtra((r && r.valid === false && r.errors && r.errors.length) ? r.errors : null, data);
2084
- };
2085
- const _buildRawErrors = (data) => {
2086
- const r = _full(data);
2087
- let raw = null;
2088
- if (r && r.valid === false) {
2089
- raw = typeof r._ataRaw === 'function' ? r._ataRaw() : r.errors;
2090
- if (!raw || !raw.length) raw = null;
2091
- }
2092
- return _withExtra(raw, data);
2093
- };
2094
- this.validate = (data) => {
2095
- if (_fast(data)) return { valid: true, data, errors: EMPTY_ERRORS };
2096
- return new LazyRejection(_buildErrors, data, _buildRawErrors);
2097
- };
2098
- }
2099
- if (_vx && !_vxApplied && this.validate) {
2100
- const inner = this.validate;
2101
- const { check, errors } = _vx;
2102
- this.validate = (data) => {
2103
- const r = inner(data);
2104
- if (r.valid && check(data)) return r;
2105
- return new ExtendedRejection(r, data, errors);
2106
- };
2107
- }
2108
-
2109
- // The buffer APIs answer from the native walker, which disagrees with
2110
- // validate() on shapes listed in lib/buffer-gate.js. For those schemas
2111
- // every buffer entry point goes through validate() instead.
2112
- if (native) {
2113
- const { bufferNeedsSlowPath, installSlowBufferApis } = require('./lib/buffer-gate.js');
2114
- if (bufferNeedsSlowPath(schemaObj, this._schemaMap, this._keywords)) installSlowBufferApis(this);
2115
- }
2116
- // Installed after the buffer gate on purpose: the gate replaces
2117
- // isValidJSON for schemas whose shapes the native walker gets wrong,
2118
- // unevaluatedProperties among them, and the scanner wiring has to wrap
2119
- // whatever answers last or a gated schema silently loses its scanner.
2120
- if (this._jsFn && !this._preprocess) {
2121
- const self = this;
2122
- // Generating a scanner costs about 20 microseconds, measured, and it
2123
- // saves from around 85 nanoseconds on a small accepted document to
2124
- // several microseconds on a rejected one. Building it on the first
2125
- // call would therefore be a straight loss for a caller that checks one
2126
- // document and exits, so it is built once a caller has asked often
2127
- // enough that it is plainly doing this in a loop. A server passes the
2128
- // line during warm-up and never sees it.
2129
- const SCAN_AFTER = 64;
2130
- let calls = 0;
2131
- // undefined: not built. null: this schema has no scanner. Passing true
2132
- // builds it now, which is how the differential test reaches it.
2133
- this._ensureScanner = (now) => {
2134
- if (self._scanner === undefined) {
2135
- if (!now && ++calls < SCAN_AFTER) return undefined;
2136
- const built = require('./lib/scan-compiler').compileScanner(schemaObj, { userFormats: self._userFormats });
2137
- self._scanner = built ? built.scan : null;
2138
- }
2139
- return self._scanner;
2140
- };
2141
- const byParsing = this.isValidJSON;
2142
- // The verdict is a pure function of the text, and the caller a
2143
- // gateway or a drift monitor keeps asking about is usually the same
2144
- // text: a config file re-read on a timer, a heartbeat body. One
2145
- // remembered (text, verdict) pair answers that case with a native
2146
- // string compare, which is a memcmp, instead of a scan. Withheld when
2147
- // user formats or custom keywords are present, since those are user
2148
- // functions and nothing guarantees they are pure.
2149
- const memoizable = !self._userFormats && !self._usesKeywords;
2150
- let _memoText = null;
2151
- let _memoVerdict = false;
2152
- this.isValidJSON = (jsonStr) => {
2153
- const scan = self._ensureScanner();
2154
- if (scan === undefined) return byParsing(jsonStr);
2155
- if (scan === null) { _bindEntry(self, 'isValidJSON', byParsing); return byParsing(jsonStr); }
2156
- _bindEntry(self, 'isValidJSON', memoizable
2157
- ? (text) => {
2158
- if (typeof text !== 'string') return byParsing(text);
2159
- if (text === _memoText) return _memoVerdict;
2160
- const r = scan(text);
2161
- const verdict = r === -1 ? byParsing(text) : r === 1;
2162
- _memoText = text;
2163
- _memoVerdict = verdict;
2164
- return verdict;
2165
- }
2166
- : (text) => {
2167
- if (typeof text !== 'string') return byParsing(text);
2168
- const r = scan(text);
2169
- if (r === -1) return byParsing(text);
2170
- return r === 1;
2171
- });
2172
- return self.isValidJSON(jsonStr);
2173
- };
2174
- // validateJSON gets the same short-circuit isValidJSON has. The verdict is
2175
- // a property of the text, and the scanner reads the text once and
2176
- // allocates nothing; above the simdjson threshold the path underneath
2177
- // encoded the whole document to a Buffer and called the native validator
2178
- // instead, which measured 340 microseconds against the scanner's 191 on a
2179
- // 149 KB config, the same against the published addon as against a local
2180
- // build.
2181
- //
2182
- // An accepted document stops at the scanner. A rejected one still has to
2183
- // produce errors, so it goes on to the path below, which is the
2184
- // rich-errors wrapper and everything under it; the flag only tells that
2185
- // path to skip an encode and a native call that would return false. Doing
2186
- // it the other way, returning errors from the inner function directly,
2187
- // would hand back errors that never passed through enrichment.
2188
- {
2189
- const validateByParsing = this.validateJSON;
2190
- const abortEarly = !!options.abortEarly;
2191
- this.validateJSON = (jsonStr) => {
2192
- const scan = self._ensureScanner();
2193
- if (scan === undefined) return validateByParsing(jsonStr);
2194
- if (scan === null) { _bindEntry(self, 'validateJSON', validateByParsing); return validateByParsing(jsonStr); }
2195
- _bindEntry(self, 'validateJSON', (text) => {
2196
- if (typeof text === 'string') {
2197
- const r = scan(text);
2198
- if (r === 1) return VALID_RESULT;
2199
- if (r === 0) {
2200
- if (abortEarly) return ABORT_EARLY_RESULT;
2201
- self._skipNativeFast = true;
2202
- try {
2203
- return validateByParsing(text);
2204
- } finally {
2205
- self._skipNativeFast = false;
2206
- }
2207
- }
2208
- }
2209
- return validateByParsing(text);
2210
- });
2211
- return self.validateJSON(jsonStr);
2212
- };
2213
- }
2214
- }
2215
-
2216
- // An extension registered through _extendValidate covers the JSON entry
2217
- // points too. They are final here except for the scanner stubs above,
2218
- // which rebind themselves on first use through _bindEntry, so the
2219
- // extension is applied to whatever each one is now and again on rebind.
2220
- if (_vx) {
2221
- this._entryExt = _jsonEntryWrappers(_vx);
2222
- for (const name of ['validateJSON', 'isValidJSON', 'validateAndParse']) {
2223
- if (Object.prototype.hasOwnProperty.call(this, name) && typeof this[name] === 'function') _bindEntry(this, name, this[name]);
2224
- }
2225
- }
2226
-
2227
-
2228
- _rememberInstance(this);
2229
- }
2230
-
2231
- // Which engine answers validate() for this schema: 'codegen' (generated
2232
- // JS), 'closure' (the closure compiler, the boolean fallback), 'native'
2233
- // (the C++ engine, only for some $dynamicRef schemas), or 'interpreter'.
2234
- // A diagnostic: the answer is the same on every engine, the cost is not.
2235
- engine() {
2236
- this._ensureCompiled();
2237
- return this._engine || 'interpreter';
2238
- }
2239
-
2240
- _ensureNative() {
2241
- if (this._nativeReady) return;
2242
- this._nativeReady = true;
2243
- if (!native) return;
2244
- let nativeSchemaStr = this._schemaStr;
2245
- if (this._schemaMap.size > 0) {
2246
- const merged = JSON.parse(this._schemaStr);
2247
- if (!merged.$defs) merged.$defs = {};
2248
- for (const [id, s] of this._schemaMap) {
2249
- merged.$defs['__ext_' + id.replace(/[^a-zA-Z0-9]/g, '_')] = s;
2250
- }
2251
- nativeSchemaStr = JSON.stringify(merged);
2252
- }
2253
- this._compiled = new native.CompiledSchema(nativeSchemaStr);
2254
- // The fast registry is a fixed array of slots in the addon, and registering
2255
- // a distinct schema past the last one throws. It is an accelerator for the
2256
- // buffer path, not a requirement, so a full registry leaves the slot at -1
2257
- // and the buffer methods answer from the JS engine instead. Letting this
2258
- // throw made every validator built after the 4096th unusable on that path.
2259
- try {
2260
- this._fastSlot = native.fastRegister(nativeSchemaStr);
2261
- } catch {
2262
- this._fastSlot = -1;
2263
- }
2264
- }
2265
-
2266
- // The buffer path without a fast slot: decode, parse, and hand the value to
2267
- // the engine that does not need one. Slower than the zero-copy walk, and the
2268
- // same answer. A negative slot must never reach rawFastValidate, whose bounds
2269
- // check would report a valid document as invalid.
2270
- _slowBufferValid(input, who) {
2271
- let text;
2272
- if (typeof input === 'string') text = input;
2273
- else if (input instanceof Uint8Array) {
2274
- text = Buffer.from(input.buffer, input.byteOffset, input.byteLength).toString('utf8');
2275
- } else {
2276
- throw new TypeError(who + '() requires a Buffer, Uint8Array, or string');
2277
- }
2278
- let parsed;
2279
- try {
2280
- parsed = JSON.parse(text);
2281
- } catch {
2282
- return false;
2283
- }
2284
- return this.isValidObject(parsed);
2285
- }
2286
-
2287
- addSchema(schema) {
2288
- if (this._initialized) {
2289
- throw new Error('Cannot add schema after compilation — call addSchema() before validate()')
2290
- }
2291
- if (!schema || !schema.$id) {
2292
- throw new Error('Schema must have $id')
2293
- }
2294
- // Normalize a copy so the caller's object is never mutated. A document
2295
- // without a dialect of its own is read under the root's draft.
2296
- const root = this._schemaObj
2297
- const rootIsDraft7 = !!(root && typeof root === 'object' && typeof root.$schema === 'string' &&
2298
- (root.$schema === 'http://json-schema.org/draft-07/schema#' || root.$schema === 'http://json-schema.org/draft-07/schema'))
2299
- const normalized = _normalizeCallerSchema(schema, rootIsDraft7)
2300
- this._ownSchemaMap()
2301
- this._schemaMap.set(normalized.$id, normalized)
2302
- }
2303
-
2304
- // buildSchemaMap hands the same map to every validator built from the same
2305
- // registry. Take a private copy before writing to it.
2306
- _ownSchemaMap() {
2307
- if (!this._schemaMapShared) return
2308
- this._schemaMap = new Map(this._schemaMap)
2309
- this._schemaMapShared = false
2310
- }
2311
-
2312
- _ensureCodegen() {
2313
- if (this._jsFn) return;
2314
- // A validator that rewrites its input cannot use the binding below: that
2315
- // one answers from the compiled function alone and would skip the rewrite,
2316
- // so isValidObject() and validate() would disagree.
2317
- if (this._needsPreprocess() || this._usesKeywords) {
2318
- this._ensureCompiled();
2319
- return;
2320
- }
2321
- this._ensureVocabularies();
2322
- if (this._interpretOnly || (typeof process !== 'undefined' && process.env && process.env.ATA_FORCE_NAPI)) return;
2323
- if (!this._schemaStr) this._schemaStr = JSON.stringify(this._schemaObj);
2324
- const sm = this._schemaMap.size > 0 ? this._schemaMap : null;
2325
- const mapKey = compileCacheKey(this._schemaStr, this._schemaMap);
2326
- // Custom formats are JS functions: skip the shared cache so different
2327
- // validators with the same schema string but different formats don't collide.
2328
- const cached = (this._userFormats || this._usesKeywords) ? null : _compileCache.get(mapKey);
2329
- if (cached && cached.jsFn) {
2330
- this._jsFn = cached.jsFn;
2331
- _bindVerdict(this, cached.jsFn);
2332
- _rememberInstance(this);
2333
- return;
2334
- }
2335
- const uf = this._userFormats;
2336
- const _cg = compileToJSCodegen(this._schemaObj, sm, uf);
2337
- const jsFn = _cg || compileToJS(this._schemaObj, null, sm);
2338
- this._jsFn = jsFn;
2339
- if (jsFn) {
2340
- _bindVerdict(this, jsFn);
2341
- _rememberInstance(this);
2342
- // A partial entry: the verdict function is real, the other two are not
2343
- // built yet rather than declined. `undefined` is the not-built marker
2344
- // the full compile's _buildErr/_buildCombined look for; `null` would read as
2345
- // "the compiler declined" and cost the schema its error function, which
2346
- // is the bug this cache had once already. `isCodegen` rides along so a
2347
- // validator that later reuses this entry reports the same engine it
2348
- // would have compiled to.
2349
- if (!uf) {
2350
- if (!cached) _compileCache.set(mapKey, { jsFn, combined: undefined, errFn: undefined, isCodegen: !!_cg, full: false });
2351
- else cached.jsFn = jsFn;
2352
- }
2353
- }
2354
- }
2355
-
2356
- // Load a pre-compiled standalone module. Zero schema compilation.
2357
- // No NAPI, no native compile — pure JS. Startup in microseconds.
2358
- // Usage: const v = Validator.fromStandalone(require('./compiled.js'), schema, opts)
2359
- static fromStandalone(mod, schema, opts) {
2360
- const options = opts || {};
2361
- const schemaObj = typeof schema === "string" ? JSON.parse(schema) : schema;
2362
-
2363
- // Create a lightweight instance — skip NAPI compile entirely
2364
- const v = Object.create(Validator.prototype);
2365
- v._jsFn = mod.boolFn;
2366
- v._compiled = null;
2367
- v._fastSlot = -1;
2368
-
2369
- // Mutators
2370
- const applyDefaults = buildDefaultsApplier(schemaObj);
2371
- const applyCoerce = options.coerceTypes ? buildCoercer(schemaObj) : null;
2372
- const applyRemove = options.removeAdditional
2373
- ? buildRemover(schemaObj)
2374
- : null;
2375
- const mutators = [applyRemove, applyCoerce, applyDefaults].filter(Boolean);
2376
- const preprocess =
2377
- mutators.length === 0
2378
- ? null
2379
- : mutators.length === 1
2380
- ? mutators[0]
2381
- : (data) => {
2382
- for (let i = 0; i < mutators.length; i++) mutators[i](data);
2383
- };
2384
- v._preprocess = preprocess;
2385
-
2386
- // Error function — use pre-compiled from standalone if available, else compile
2387
- let errFn = (d) => ({
2388
- valid: false,
2389
- errors: [
2390
- { code: "validation_failed", path: "", message: "validation failed" },
2391
- ],
2392
- });
2393
- if (mod.errFn) {
2394
- errFn = (d) => mod.errFn(d, true);
2395
- } else {
2396
- const jsErrFn = compileToJSCodegenWithErrors(schemaObj);
2397
- if (jsErrFn) {
2398
- try {
2399
- jsErrFn({}, true);
2400
- errFn = (d) => jsErrFn(d, true);
2401
- } catch {}
2402
- }
2403
- }
2404
-
2405
- // Hybrid or speculative
2406
- const hybridFn = mod.hybridFactory
2407
- ? mod.hybridFactory(VALID_RESULT, errFn)
2408
- : null;
2409
-
2410
- v.validate = hybridFn
2411
- ? preprocess
2412
- ? (data) => {
2413
- preprocess(data);
2414
- return hybridFn(data);
2415
- }
2416
- : hybridFn
2417
- : preprocess
2418
- ? (data) => {
2419
- preprocess(data);
2420
- return mod.boolFn(data) ? VALID_RESULT : errFn(data);
2421
- }
2422
- : (data) => (mod.boolFn(data) ? VALID_RESULT : errFn(data));
2423
- {
2424
- const _bare = v.validate;
2425
- v.validate = (data) => {
2426
- const r = _bare(data);
2427
- return (r.valid === true && r.data === undefined)
2428
- ? { valid: true, data, errors: r.errors }
2429
- : r;
2430
- };
2431
- }
2432
- v.isValidObject = mod.boolFn;
2433
- v.isValidJSON = (jsonStr) => {
2434
- try {
2435
- return mod.boolFn(JSON.parse(jsonStr));
2436
- } catch {
2437
- return false;
2438
- }
2439
- };
2440
- v.validateJSON = (jsonStr) => {
2441
- try {
2442
- const obj = JSON.parse(jsonStr);
2443
- return hybridFn
2444
- ? hybridFn(obj)
2445
- : mod.boolFn(obj)
2446
- ? VALID_RESULT
2447
- : errFn(obj);
2448
- } catch {
2449
- return {
2450
- valid: false,
2451
- errors: [{ code: "invalid_json", path: "", message: "invalid JSON" }],
2452
- };
2453
- }
2454
- };
2455
-
2456
- v.validateAndParse = native
2457
- ? (jsonStr) => {
2458
- v._ensureNative();
2459
- v.validateAndParse = (s) => v._compiled.validateAndParse(s);
2460
- return v.validateAndParse(jsonStr);
2461
- }
2462
- : () => { throw new Error('Native addon required for validateAndParse()'); };
2463
-
2464
- // Standard Schema V1
2465
- Object.defineProperty(v, "~standard", {
2466
- value: Object.freeze({
2467
- version: 1,
2468
- vendor: "ata-validator",
2469
- validate(value) {
2470
- const result = v.validate(value);
2471
- if (result.valid) return { value };
2472
- return {
2473
- issues: result.errors.map((e) => ({
2474
- message: e.message,
2475
- path: parsePointerPath(e.instancePath),
2476
- })),
2477
- };
2478
- },
2479
- }),
2480
- writable: false,
2481
- enumerable: false,
2482
- configurable: false,
2483
- });
2484
-
2485
- return v;
2486
- }
2487
-
2488
- // Raw NAPI fast path for Buffer/Uint8Array
2489
- isValid(input) {
2490
- if (!native) throw new Error('Native addon required for isValid() — install build tools or use validate() instead');
2491
- if (typeof input === 'string') input = Buffer.from(input);
2492
- else if (!(input instanceof Uint8Array)) throw new TypeError('isValid() requires a Buffer, Uint8Array, or string. For parsed objects, use isValidObject().');
2493
- this._ensureNative();
2494
- return native.rawFastValidate(this._fastSlot, input);
2495
- }
2496
-
2497
- // Zero-copy pre-padded path
2498
- isValidPrepadded(paddedBuffer, jsonLength) {
2499
- if (!native) throw new Error('Native addon required for isValidPrepadded()');
2500
- this._ensureNative();
2501
- return native.rawFastValidate(this._fastSlot, paddedBuffer, jsonLength);
2502
- }
2503
-
2504
- // Parallel NDJSON batch (multi-core)
2505
- isValidParallel(buffer) {
2506
- if (!native) throw new Error('Native addon required for isValidParallel()');
2507
- this._ensureNative();
2508
- return native.rawParallelValidate(this._fastSlot, buffer);
2509
- }
2510
-
2511
- // Parallel count (fastest -- single uint32 return)
2512
- countValid(buffer) {
2513
- if (!native) throw new Error('Native addon required for countValid()');
2514
- this._ensureNative();
2515
- return native.rawParallelCount(this._fastSlot, buffer);
2516
- }
2517
-
2518
- // NDJSON single-thread batch
2519
- isValidNDJSON(buffer) {
2520
- if (!native) throw new Error('Native addon required for isValidNDJSON()');
2521
- this._ensureNative();
2522
- return native.rawNDJSONValidate(this._fastSlot, buffer);
2523
- }
2524
- }
2525
-
2526
- // One-shot validate. It goes through a Validator like every other entry
2527
- // point, so the result has one shape everywhere: `data` on success, errors
2528
- // with a code, a keyword and an instancePath. From the first native binding
2529
- // until 1.33.3 it handed the schema straight to the native engine whenever
2530
- // the addon was loaded, which is every default install on a supported
2531
- // platform, and returned that engine's raw result: numeric codes, `path`
2532
- // instead of `instancePath`, no keyword, and no `data`. The compile cache keeps
2533
- // a schema passed again from compiling again.
2534
- function validate(schema, data) {
2535
- if (schema instanceof Validator) return schema.validate(data);
2536
- const v = new Validator(typeof schema === "string" ? JSON.parse(schema) : schema);
2537
- return v.validate(data);
2538
- }
2539
-
2540
- // Async validation for schemas built with `t.refine(...)`. Structural
2541
- // validation runs synchronously first; refinements are awaited only when the
2542
- // value is structurally valid (a refinement body may assume the right shape).
2543
- // Accepts a schema literal or an existing Validator instance plus its schema.
2544
- // Returns a Promise<ValidationResult>.
2545
- async function validateAsync(schemaOrValidator, data) {
2546
- const refineLib = require('./lib/refine');
2547
- let validator, schema;
2548
- if (schemaOrValidator instanceof Validator) {
2549
- validator = schemaOrValidator;
2550
- schema = validator._schemaObj;
2551
- } else {
2552
- schema = schemaOrValidator;
2553
- validator = new Validator(schema);
2554
- }
2555
- const structural = validator.validate(data);
2556
- if (!structural.valid) return structural;
2557
- const refinements = refineLib.getRefinements(schema);
2558
- if (!refinements) return structural;
2559
- const issues = await refineLib.runRefinements(refinements, structural.data !== undefined ? structural.data : data);
2560
- if (issues.length) return { valid: false, errors: issues };
2561
- return structural;
2562
- }
2563
-
2564
- // parseAsync resolves to the validated data, or rejects with an Error whose
2565
- // `.errors` carries the ValidationError list. Mirrors the parse/validate split
2566
- // used by Zod-style callers.
2567
- async function parseAsync(schemaOrValidator, data) {
2568
- const result = await validateAsync(schemaOrValidator, data);
2569
- if (result.valid) return result.data !== undefined ? result.data : data;
2570
- const err = new Error('ata: async validation failed');
2571
- err.errors = result.errors;
2572
- throw err;
2573
- }
2574
-
2575
- function version() {
2576
- if (native) return native.version();
2577
- try { return require("./lib/version"); } catch { return "unknown"; }
2578
- }
2579
-
2580
- // Static AOT entry points are thin lazy-loaders into `lib/aot.js`. The
2581
- // implementation files (and the `fs`/`path` reads they perform) only enter
2582
- // the process when one of these is actually called. See `lib/aot.js` for the
2583
- // generated module shapes; browser bundles get `lib/aot.browser.js` (a stub
2584
- // that throws) via the package.json `browser` field.
2585
- Validator.bundle = function (schemas, opts) {
2586
- return require('./lib/aot.js').bundle(Validator, schemas, opts);
2587
- };
2588
-
2589
- Validator.bundleStandalone = function (schemas, opts) {
2590
- return require('./lib/aot.js').bundleStandalone(Validator, schemas, opts);
2591
- };
2592
-
2593
- Validator.bundleCompact = function (schemas, opts) {
2594
- return require('./lib/aot.js').bundleCompact(Validator, schemas, opts);
2595
- };
2596
-
2597
- Validator.loadBundle = function (mods, schemas, opts) {
2598
- return require('./lib/aot.js').loadBundle(Validator, mods, schemas, opts);
2599
- };
2600
-
2601
- const parseJSON = native ? native.parseJSON : JSON.parse;
2602
-
2603
- // Ultra-fast compile: returns validate function directly, no Validator wrapper
2604
- // WeakMap cached — second call with same schema object is ~3ns
2605
- const _compileFnCache = new WeakMap();
2606
- function compile(schema, opts) {
2607
- if (!opts && typeof schema === 'object' && schema !== null) {
2608
- const hit = _compileFnCache.get(schema);
2609
- if (hit) return hit;
2610
- }
2611
- const v = new Validator(schema, opts);
2612
- v._ensureCompiled();
2613
- const fn = v.validate;
2614
- if (!opts && typeof schema === 'object' && schema !== null) {
2615
- _compileFnCache.set(schema, fn);
2616
- }
2617
- return fn;
2618
- }
2619
-
2620
- const { toTypeScript } = require("./lib/ts-gen");
2621
- const { renderPretty } = require("./lib/render-pretty");
2622
- const { renderCompact } = require("./lib/render-compact");
2623
- const { toOutput } = require("./lib/output-format");
2624
- const { toRetryMessage } = require("./lib/retry-message");
2625
- const { describeSchema } = require("./lib/describe-schema");
2626
- const { renderJSON } = require("./lib/render-json");
2627
- const { suggestFor } = require("./lib/suggestions");
2628
- const { reprValue } = require("./lib/enrich-error");
2629
-
2630
- // Walk a JSON pointer (RFC 6901 escapes) into a data tree. Mirrors the helper
2631
- // inside lib/suggestions.js — kept local to avoid exporting an internal.
2632
- function _walkPointer (root, pointer) {
2633
- if (!pointer) return root;
2634
- const parts = pointer.replace(/^\//, '').split('/').map(s => s.replace(/~1/g, '/').replace(/~0/g, '~'));
2635
- let cur = root;
2636
- for (const p of parts) { if (cur == null) return undefined; cur = cur[p]; }
2637
- return cur;
2638
- }
2639
-
2640
- // Post-hoc suggestion enrichment for AOT-compiled validators. The standalone
2641
- // modules do not embed the suggestion engine (Levenshtein + format hints would
2642
- // inflate the gzipped bundle beyond the size budget). Consumers who want
2643
- // suggestions pass the error array through this helper after validation.
2644
- // AOT errors don't carry `received`, so we re-derive it from `data` here.
2645
- const { setDiagnosticSource } = require('./lib/diagnostic-source');
2646
- const attachDiagnosticSource = setDiagnosticSource;
2647
-
2648
- // Resolved once. A require() inside the function was re-resolving the path
2649
- // on every rejection, which the profile showed as internalModuleStat at the
2650
- // top of the reject path, above the correlation it was loading.
2651
- let _correlateTypos = null;
2652
- function attachRelated (errors) {
2653
- if (_correlateTypos === null) _correlateTypos = require('./lib/correlate').correlateTypos;
2654
- const pairs = _correlateTypos(errors);
2655
- if (pairs === null) return errors;
2656
- for (const [from, to] of pairs) {
2657
- const e = errors[from];
2658
- if (!e) continue;
2659
- if (e.related) { if (!e.related.includes(to)) e.related.push(to); }
2660
- else e.related = [to];
2661
- }
2662
- return errors;
2663
- }
2664
-
2665
- function attachSuggestions (errors, data) {
2666
- if (!errors) return errors;
2667
- for (const e of errors) {
2668
- if (!e || e.suggestion) continue;
2669
- let received = e.received;
2670
- if (received === undefined && data !== undefined) {
2671
- const ptr = e.instancePath != null ? e.instancePath : (e.path || '');
2672
- const raw = _walkPointer(data, ptr);
2673
- if (raw !== undefined || ptr === '') received = reprValue(raw);
2674
- }
2675
- const probe = received !== undefined && e.received === undefined
2676
- ? Object.assign({}, e, { received })
2677
- : e;
2678
- const s = suggestFor(probe, data);
2679
- if (s) e.suggestion = s;
2680
- }
2681
- // AOT modules import nothing, so this is their only route to a frame. The
2682
- // caller holds the original object and ran no preprocessing through here.
2683
- attachDiagnosticSource(errors, { data, mutatesInput: false });
2684
- return errors;
2685
- }
2686
-
2687
- // Authoring helper: identity at runtime. Its only job is to attach the
2688
- // JSONSchema type (see index.d.ts) to an inline schema object so TypeScript
2689
- // gives autocomplete and value checking while authoring. Returns the schema
2690
- // untouched so it can be passed straight to Validator, toStandaloneModule, etc.
2691
- function defineSchema (schema) {
2692
- return schema;
2693
- }
2694
-
2695
- // Public methods start as memoized accessors on the prototype. A fresh
2696
- // Validator allocates none of them; the first read of a method builds the
2697
- // bound closure, stores it on the instance as an ordinary writable property
2698
- // and returns it. The setter keeps the compile step's plain assignments
2699
- // (`this.validate = fn`) working before the getter has ever run. Detached
2700
- // use (`const f = v.validate`) keeps working because the closure binds the
2701
- // instance.
2702
- // Standard Schema V1. Built on first read, then pinned to the instance with
2703
- // the same descriptor the constructor used to install eagerly.
2704
- Object.defineProperty(Validator.prototype, "~standard", {
2705
- configurable: true,
2706
- get() {
2707
- const self = this;
2708
- const std = Object.freeze({
2709
- version: 1,
2710
- vendor: "ata-validator",
2711
- validate(value) {
2712
- const result = self.validate(value);
2713
- if (result.valid) {
2714
- return { value };
2715
- }
2716
- // An issue carries a message and a path and nothing else, so the
2717
- // suggestion and source-frame work the rich error path does would be
2718
- // thrown away here. Take the raw, schema-ordered list when the result
2719
- // offers one; fall back to the public list otherwise.
2720
- const raw = typeof result._ataRaw === 'function' ? result._ataRaw() : result.errors;
2721
- const issues = new Array(raw.length);
2722
- for (let i = 0; i < raw.length; i++) {
2723
- const err = raw[i];
2724
- const path = err.instancePath != null ? err.instancePath : (err.path || '');
2725
- let message = err.message;
2726
- if (!message) {
2727
- // The native engine reports numeric codes without a message; the
2728
- // enrich pass knows how to word those. Rare, so required lazily.
2729
- message = require('./lib/enrich-error').enrich(err, {}).message;
2730
- }
2731
- issues[i] = { message, path: parsePointerPath(path) };
2732
- }
2733
- return { issues };
2734
- },
2735
- });
2736
- Object.defineProperty(this, "~standard", {
2737
- value: std,
2738
- writable: false,
2739
- enumerable: false,
2740
- configurable: false,
2741
- });
2742
- return std;
2743
- },
2744
- });
2745
-
2746
- // The error resolvers run only after a verdict function has said no. If one
2747
- // answers valid anyway, two generators disagree, and the verdict is the one
2748
- // to keep: returning the resolver's answer is how a vacuous combined function
2749
- // turned a rejection into an acceptance in validateJSON. The disagreement is
2750
- // reported as a generic failure rather than hidden.
2751
- const _VERDICT_DISAGREES = Object.freeze({
2752
- valid: false,
2753
- errors: Object.freeze([Object.freeze({ keyword: 'validation', instancePath: '', schemaPath: '#', params: Object.freeze({}), message: 'schema validation failed' })]),
2754
- });
2755
- function _mustReject(r) {
2756
- return r && r.valid === false ? r : _VERDICT_DISAGREES;
2757
- }
2758
-
2759
- // Install the verdict method. Every place that binds isValidObject comes
2760
- // through here, so a check registered with _extendVerdict survives the method
2761
- // being replaced as the validator compiles further, which it does more than
2762
- // once over its life.
2763
- function _bindVerdict(self, fn) {
2764
- const resolve = self._verdictTail;
2765
- if (resolve !== null && typeof fn === 'function') {
2766
- const tail = resolve();
2767
- if (typeof tail === 'function') fn = _fuseTail(fn, tail);
2768
- }
2769
- self.isValidObject = fn;
2770
- }
2771
-
2772
- // Bind one of the JSON entry points, through the extension wrapper when there is one.
2773
- function _bindEntry(self, name, fn) {
2774
- const ext = self._entryExt;
2775
- self[name] = ext !== null && ext[name] ? ext[name](fn) : fn;
2776
- }
2777
-
2778
- // The JSON entry points under an extension: the schema answers first, and only
2779
- // text it accepts is parsed for the check, so a rejection costs nothing extra.
2780
- function _jsonEntryWrappers({ check, errors }) {
2781
- const parse = (text) => JSON.parse(typeof text === 'string' ? text : new TextDecoder().decode(text));
2782
- return {
2783
- validateJSON: (inner) => (text) => {
2784
- const res = inner(text);
2785
- if (!res.valid) return res;
2786
- let data;
2787
- try { data = parse(text); } catch { return res; }
2788
- if (check(data)) return res;
2789
- const e = errors(data);
2790
- return e && e.length ? { valid: false, errors: e } : { valid: false, errors: [_EXT_FALLBACK] };
2791
- },
2792
- isValidJSON: (inner) => (text) => {
2793
- if (!inner(text)) return false;
2794
- let data;
2795
- try { data = parse(text); } catch { return true; }
2796
- return check(data);
2797
- },
2798
- validateAndParse: (inner) => (text) => {
2799
- const res = inner(text);
2800
- if (!res.valid) return res;
2801
- if (check(res.value)) return res;
2802
- const e = errors(res.value);
2803
- return { valid: false, value: res.value, errors: e && e.length ? e : [_EXT_FALLBACK] };
2804
- },
2805
- };
2806
- }
2807
- const _EXT_FALLBACK = Object.freeze({ keyword: 'validation', instancePath: '', schemaPath: '#', params: {}, message: 'schema validation failed' });
2808
-
2809
- // A verdict function that also runs `tail` on what it accepts. The generated
2810
- // function can take the check in place of its final `return true`, one call
2811
- // per document; anything else is composed.
2812
- function _fuseTail(fn, tail) {
2813
- const fused = typeof fn._withTail === 'function' ? fn._withTail(tail) : null;
2814
- return fused || ((d) => fn(d) && tail(d));
2815
- }
2816
-
2817
- // The rejection validate() returns on the paths where an extension check could
2818
- // not join the lazy layer: the inner result, plus the check's errors appended
2819
- // on first read. `inner` may itself be valid, when only the check failed.
2820
- class ExtendedRejection {
2821
- constructor(inner, data, collect) {
2822
- this.valid = false;
2823
- this._inner = inner;
2824
- this._data = data;
2825
- this._collect = collect;
2826
- this._errors = null;
2827
- }
2828
- toJSON() {
2829
- return { valid: false, errors: this.errors };
2830
- }
2831
- _ataRaw() {
2832
- const inner = this._inner;
2833
- const more = this._collect(this._data) || [];
2834
- const raw = inner.valid ? more : (typeof inner._ataRaw === 'function' ? inner._ataRaw() : inner.errors).concat(more);
2835
- return raw.length ? raw : [_EXT_FALLBACK];
2836
- }
2837
- }
2838
- Object.defineProperty(ExtendedRejection.prototype, 'errors', {
2839
- enumerable: true,
2840
- configurable: true,
2841
- get() {
2842
- if (this._errors === null) {
2843
- const inner = this._inner;
2844
- const more = this._collect(this._data) || [];
2845
- const all = inner.valid ? more : inner.errors.concat(more);
2846
- this._errors = all.length ? all : [_EXT_FALLBACK];
2847
- }
2848
- return this._errors;
2849
- },
2850
- });
2851
-
2852
- // For wrappers that enforce a check the schema does not carry, such as the
2853
- // `instanceof` keyword of @ata-project/keywords. `resolve` is called whenever
2854
- // the verdict method is bound, which is after the schema has been normalized,
2855
- // and returns the check, a function of the document that answers true or
2856
- // false, or null when there is nothing to add. Only isValidObject takes it;
2857
- // the other entry points, which report errors, stay the wrapper's to handle.
2858
- //
2859
- // Before this, such a wrapper had to hold isValidObject behind an accessor so
2860
- // that the validator's own rebinding could not drop its check, and every call
2861
- // paid for the accessor and two more calls: 10.1 ns against 4.2 on a document
2862
- // the schema rejects at its third property.
2863
- Validator.prototype._verdictTail = null;
2864
- Validator.prototype._validateTail = null;
2865
- Validator.prototype._entryExt = null;
2866
-
2867
- // Let `new Validator(schema)` with the same schema object return this instance.
2868
- // Only an instance built without options may answer that call: one built with
2869
- // options (richErrors: false, coerceTypes, formats, ...) would hand its options
2870
- // to a caller that asked for none, and an extended one would enforce checks the
2871
- // caller never registered. Both the caller's object and the normalized one are
2872
- // keys, since a later caller passes the former.
2873
- function _rememberInstance(self) {
2874
- if (!self._noOpts || self._verdictTail !== null || self._validateTail !== null) return;
2875
- const raw = self._rawSchema;
2876
- if (raw && typeof raw === 'object' && !_identityCache.has(raw)) _identityCache.set(raw, self);
2877
- const obj = self._schemaObj;
2878
- if (obj !== raw && obj && typeof obj === 'object' && !_identityCache.has(obj)) _identityCache.set(obj, self);
2879
- }
2880
-
2881
- // An extended validator answers differently from a plain one for the same
2882
- // schema, so it must not be the instance `new Validator(sameSchema)` hands out.
2883
- function _leaveIdentityCache(self) {
2884
- self._noOpts = false;
2885
- // Nothing is registered before the first compile, so there is nothing to
2886
- // take back.
2887
- if (!self._initialized && self._jsFn === null) return;
2888
- const raw = self._rawSchema;
2889
- if (raw && typeof raw === 'object' && _identityCache.get(raw) === self) _identityCache.delete(raw);
2890
- // The compiled form is cached too, at the end of the first compile. Read it
2891
- // only if it is already materialized: reading it otherwise builds it.
2892
- if (Object.prototype.hasOwnProperty.call(self, '_schemaObj')) {
2893
- const obj = self._schemaObj;
2894
- if (obj && typeof obj === 'object' && _identityCache.get(obj) === self) _identityCache.delete(obj);
2895
- }
2896
- }
2897
-
2898
- // The same kind of extension for validate(): `resolve` returns { check, errors }
2899
- // or null, where `errors(data)` lists the check's own errors, or returns null
2900
- // when there are none. The check's errors come after the schema's, and a value
2901
- // that fails only the check is rejected with the check's errors alone. Must be
2902
- // called before validate() is first used, which is when it is compiled; later
2903
- // calls throw rather than being silently ignored.
2904
- Validator.prototype._extendValidate = function (resolve) {
2905
- if (typeof resolve !== 'function') throw new TypeError('_extendValidate expects a function');
2906
- if (this._initialized) throw new Error('_extendValidate must be called before the validator compiles');
2907
- _leaveIdentityCache(this);
2908
- const prev = this._validateTail;
2909
- this._validateTail = prev === null ? resolve : () => {
2910
- const a = prev(), b = resolve();
2911
- if (!a) return b;
2912
- if (!b) return a;
2913
- return {
2914
- check: (d) => a.check(d) && b.check(d),
2915
- errors: (d) => {
2916
- const x = a.errors(d), y = b.errors(d);
2917
- if (!x) return y;
2918
- if (!y) return x;
2919
- return x.concat(y);
2920
- },
2921
- };
2922
- };
2923
- return this;
2924
- };
2925
-
2926
- // Both extensions in one call, from one resolver that returns { check, errors }
2927
- // or null. This is the form @ata-project/keywords uses: registering costs one
2928
- // closure, where wrapping the five entry points cost a closure per entry point,
2929
- // the wrappers themselves and an accessor, most of what building a wrapped
2930
- // validator took.
2931
- // parse(data): validate, then return a copy holding only what the schema
2932
- // declares, the way the parse() export of an ahead-of-time module does, with
2933
- // the same emitter behind both. Building the copy from the schema's own key
2934
- // list costs less than finding and deleting unknown keys, and leaves the
2935
- // caller's object alone. Where the key set cannot be proven (a $ref it cannot
2936
- // inline, patternProperties, an open object) the method declines with an
2937
- // error instead of guessing, as the module ships no parse() there; so it does
2938
- // under options that rewrite input, whose answers a copy would not reproduce.
2939
- Validator.prototype.parse = function (data) {
2940
- let fn = this._parseFn;
2941
- if (fn === undefined) {
2942
- fn = _buildParse(this);
2943
- Object.defineProperty(this, '_parseFn', { value: fn, writable: true, configurable: true, enumerable: false });
2944
- }
2945
- return fn(data);
2946
- };
2947
-
2948
- function _buildParse(self) {
2949
- const decline = (why, instead = 'Use validate() with removeAdditional instead.') => () => {
2950
- throw new TypeError(`parse() is not available for this validator: ${why}. ${instead}`);
2951
- };
2952
- const o = self._options;
2953
- // Each refusal names its own reason: a caller who hit the combined one could
2954
- // not tell a coercion option from a keyword package, and read a deliberate
2955
- // refusal as a failure of valid data.
2956
- if (o.coerceTypes) return decline('coerceTypes rewrites the input before it is checked');
2957
- if (o.removeAdditional === 'all') return decline("removeAdditional: 'all' decides the kept keys at check time");
2958
- if (self._usesKeywords) return decline('custom keywords are in use, and what they accept is not known to the copy');
2959
- // Checks added to the validator (withKeywords from @ata-project/keywords
2960
- // adds instanceof and typeof) only narrow what passes; they do not change
2961
- // which keys the schema declares. The verdict then goes through
2962
- // isValidObject, which runs them, and a property they check with instanceof
2963
- // is carried over as it is (see clone-emit).
2964
- const extended = self._verdictTail !== null || self._validateTail !== null;
2965
- const { cloneExprFor } = require('./lib/clone-emit');
2966
- const expr = cloneExprFor(self._schemaObj);
2967
- if (!expr) return decline('the set of keys to keep cannot be proven from the schema');
2968
- let copy;
2969
- try {
2970
- // eslint-disable-next-line no-new-func
2971
- copy = new Function('data', 'return ' + expr);
2972
- } catch {
2973
- return decline('code generation is not allowed here');
2974
- }
2975
- self._ensureCompiled();
2976
- const verdict = extended ? (d) => self.isValidObject(d) : self._jsFn;
2977
- if (typeof self._jsFn !== 'function') return decline('the schema has no generated verdict function');
2978
- return (data) => {
2979
- if (!verdict(data)) {
2980
- const e = new Error('validation failed');
2981
- e.name = 'AtaValidationError';
2982
- let target = data;
2983
- if (self._mutatesInput) {
2984
- try { target = structuredClone(data); } catch { target = data; }
2985
- }
2986
- e.errors = self.validate(target).errors;
2987
- throw e;
2988
- }
2989
- return copy(data);
2990
- };
2991
- }
2992
-
2993
- Validator.prototype._extendChecks = function (resolve) {
2994
- if (typeof resolve !== 'function') throw new TypeError('_extendChecks expects a function');
2995
- this._extendValidate(resolve);
2996
- return this._extendVerdict(() => {
2997
- const x = resolve();
2998
- return x ? x.check : null;
2999
- });
3000
- };
3001
-
3002
- // Whether this instance enforces a check its schema does not carry. The
3003
- // ahead-of-time emitters build a module from the schema alone, so they refuse
3004
- // an instance that says yes rather than emit one that accepts too much.
3005
- // Reading it resolves the registered checks; only an emitter reads it.
3006
- Object.defineProperty(Validator.prototype, '_externalChecks', {
3007
- configurable: true,
3008
- get() {
3009
- if (this._validateTail !== null && this._validateTail()) return true;
3010
- if (this._verdictTail !== null && typeof this._verdictTail() === 'function') return true;
3011
- return false;
3012
- },
3013
- });
3014
-
3015
- Validator.prototype._extendVerdict = function (resolve) {
3016
- if (typeof resolve !== 'function') throw new TypeError('_extendVerdict expects a function');
3017
- _leaveIdentityCache(this);
3018
- const prev = this._verdictTail;
3019
- this._verdictTail = prev === null ? resolve : () => {
3020
- const a = prev(), b = resolve();
3021
- if (typeof a !== 'function') return b;
3022
- if (typeof b !== 'function') return a;
3023
- return (d) => a(d) && b(d);
3024
- };
3025
- // A method bound before this call was bound without the check. Rebind it.
3026
- // An unbound one binds through _bindVerdict on its first call.
3027
- if (Object.prototype.hasOwnProperty.call(this, 'isValidObject')) _bindVerdict(this, this.isValidObject);
3028
- return this;
3029
- };
3030
-
3031
- function _defineLazyMethod(name, maker) {
3032
- Object.defineProperty(Validator.prototype, name, {
3033
- configurable: true,
3034
- get() {
3035
- const fn = maker(this);
3036
- Object.defineProperty(this, name, { value: fn, writable: true, configurable: true, enumerable: true });
3037
- return fn;
3038
- },
3039
- set(fn) {
3040
- Object.defineProperty(this, name, { value: fn, writable: true, configurable: true, enumerable: true });
3041
- },
3042
- });
3043
- }
3044
-
3045
- for (const [name, pick] of [
3046
- ['_schemaObj', (self) => _materializeSchema(self)],
3047
- ['_usesKeywords', (self) => { _materializeSchema(self); return self._usesKeywords; }],
3048
- ['_schemaIsCallers', (self) => { _materializeSchema(self); return self._schemaIsCallers; }],
3049
- ]) {
3050
- Object.defineProperty(Validator.prototype, name, {
3051
- configurable: true,
3052
- get() { return pick(this); },
3053
- set(v) { Object.defineProperty(this, name, { value: v, writable: true, configurable: true, enumerable: true }); },
3054
- });
3055
- }
3056
-
3057
- _defineLazyMethod('validate', (self) => (data) => {
3058
- self._ensureCompiled();
3059
- return self.validate(data);
3060
- });
3061
- _defineLazyMethod('isValidObject', (self) => (data) => {
3062
- // A validator that rewrites its input goes through the full compile, which
3063
- // binds a verdict method that runs the rewrite first. So does one whose
3064
- // schema uses a custom keyword: neither the tier-0 plan nor the code
3065
- // generator knows the keyword, and either would accept what validate()
3066
- // rejects.
3067
- if (self._needsPreprocess() || self._usesKeywords) {
3068
- self._ensureCompiled();
3069
- return self.isValidObject(data);
3070
- }
3071
- // Lazy: classify + build tier 0 plan on first call, not in constructor.
3072
- const _tier = classify(self._schemaObj);
3073
- if (_tier.tier === 0) {
3074
- const _plan = buildTier0Plan(self._schemaObj);
3075
- let _n = 0;
3076
- _bindVerdict(self, (d) => {
3077
- const r = tier0Validate(_plan, d);
3078
- if (++_n === 2) {
3079
- try { self._ensureCodegen(); } catch {}
3080
- }
3081
- return r;
3082
- });
3083
- } else {
3084
- // `new Function` is a property of the realm, not of the schema: under a
3085
- // strict CSP or `--disallow-code-generation-from-strings` this throws
3086
- // rather than declining, and the EvalError reached the caller of a verdict
3087
- // method. The full compile can answer without code generation, so fall
3088
- // through to it. The tier-0 branch above already guarded its own call.
3089
- try { self._ensureCodegen(); } catch { /* no codegen in this realm */ }
3090
- // Codegen can bail on shapes it cannot represent; the full compile
3091
- // binds the native path or the unsupported thrower instead of
3092
- // leaving this stub to re-dispatch to itself.
3093
- if (!self._jsFn) self._ensureCompiled();
3094
- }
3095
- return self.isValidObject(data);
3096
- });
3097
- _defineLazyMethod('validateJSON', (self) => (jsonStr) => {
3098
- self._ensureCompiled();
3099
- return self.validateJSON(jsonStr);
3100
- });
3101
- _defineLazyMethod('isValidJSON', (self) => (jsonStr) => {
3102
- self._ensureCompiled();
3103
- return self.isValidJSON(jsonStr);
3104
- });
3105
- _defineLazyMethod('validateAndParse', (self) => (jsonStr) => {
3106
- if (!native) throw new Error('Native addon required for validateAndParse()');
3107
- self._ensureCompiled();
3108
- return self.validateAndParse(jsonStr);
3109
- });
3110
- _defineLazyMethod('isValid', (self) => (buf) => {
3111
- if (!native) throw new Error('Native addon required for isValid() — use validate() or isValidObject() instead');
3112
- self._ensureCompiled();
3113
- return self.isValid(buf);
3114
- });
3115
- _defineLazyMethod('countValid', (self) => (ndjsonBuf) => {
3116
- if (!native) throw new Error('Native addon required for countValid()');
3117
- self._ensureCompiled();
3118
- return self.countValid(ndjsonBuf);
3119
- });
3120
- _defineLazyMethod('batchIsValid', (self) => (buffers) => {
3121
- if (!native) throw new Error('Native addon required for batchIsValid()');
3122
- self._ensureCompiled();
3123
- return self.batchIsValid(buffers);
3124
- });
3125
-
3126
832
  module.exports = {
3127
- Validator,
3128
- compile,
3129
- validate,
3130
- validateAsync,
3131
- parseAsync,
3132
- version,
3133
- createPaddedBuffer,
3134
- SIMDJSON_PADDING,
3135
- parseJSON,
833
+ Validator: core.Validator,
834
+ compile: core.compile,
835
+ validate: core.validate,
836
+ validateAsync: core.validateAsync,
837
+ parseAsync: core.parseAsync,
838
+ version: core.version,
839
+ createPaddedBuffer: core.createPaddedBuffer,
840
+ SIMDJSON_PADDING: core.SIMDJSON_PADDING,
841
+ parseJSON: core.parseJSON,
3136
842
  toTypeScript,
3137
- defineSchema,
843
+ defineSchema: core.defineSchema,
3138
844
  renderPretty,
3139
845
  renderCompact,
3140
846
  toOutput,