@amritk/generate-validators 0.11.8 → 0.11.9

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.
@@ -1,1886 +1,1287 @@
1
- import { escapeRegexPattern } from '@amritk/helpers/escape-regex-pattern';
2
- import { getMjstInstanceOf, getMjstPrimitive } from '@amritk/helpers/mjst-extension';
3
- import { multipleOfFailExpr, multipleOfPassExpr } from '@amritk/helpers/multiple-of-check';
4
- import { refToName } from '@amritk/helpers/ref-to-name';
5
- import { safeAccessor } from '@amritk/helpers/safe-accessor';
6
- import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasDependentRequired, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMaxProperties, hasMinItems, hasMinimum, hasMinLength, hasMinProperties, hasMultipleOf, hasOneOf, hasPattern, hasProperties, hasPropertyNames, hasRef, hasRequired, hasStrictExclusiveMaximum, hasStrictExclusiveMinimum, hasType, hasUniqueItems, isObjectSchema, isSchemaObject, } from '@amritk/helpers/schema-guards';
7
- import { unknownKeyCheck } from '@amritk/helpers/unknown-key-check';
8
- /**
9
- * Derives the validator function name from a type name.
10
- * e.g. "InfoObject" → "validateInfoObject"
11
- */
1
+ import { escapeRegexPattern } from "@amritk/helpers/escape-regex-pattern";
2
+ import { getMjstInstanceOf, getMjstPrimitive } from "@amritk/helpers/mjst-extension";
3
+ import { multipleOfFailExpr, multipleOfPassExpr } from "@amritk/helpers/multiple-of-check";
4
+ import { refToName } from "@amritk/helpers/ref-to-name";
5
+ import { safeAccessor } from "@amritk/helpers/safe-accessor";
6
+ import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasDependentRequired, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMaxProperties, hasMinItems, hasMinimum, hasMinLength, hasMinProperties, hasMultipleOf, hasOneOf, hasPattern, hasProperties, hasPropertyNames, hasRef, hasRequired, hasStrictExclusiveMaximum, hasStrictExclusiveMinimum, hasType, hasUniqueItems, isObjectSchema, isSchemaObject } from "@amritk/helpers/schema-guards";
7
+ import { unknownKeyCheck } from "@amritk/helpers/unknown-key-check";
12
8
  const validatorName = (typeName) => `validate${typeName}`;
13
- /**
14
- * Returns the TypeScript typeof string for a JSON Schema primitive type.
15
- */
16
9
  const typeofString = (type) => {
17
- if (type === 'integer')
18
- return 'number';
19
- return type;
10
+ if (type === "integer")
11
+ return "number";
12
+ return type;
20
13
  };
21
- /**
22
- * Generates the inline condition that is TRUE when `accessor` does NOT equal the
23
- * `const` value. Primitives compare with `!==`; objects/arrays compare with the
24
- * runtime `valuesEqual` helper so a reordered-but-equal value still matches (the
25
- * interpreter uses order-independent deep equality, and `JSON.stringify` would
26
- * disagree because it is key-order sensitive).
27
- */
28
14
  const constMismatchCondition = (accessor, value) => {
29
- if (value === null || typeof value !== 'object') {
30
- return `${accessor} !== ${JSON.stringify(value)}`;
31
- }
32
- return `!valuesEqual(${accessor}, ${JSON.stringify(value)})`;
15
+ if (value === null || typeof value !== "object") {
16
+ return `${accessor} !== ${JSON.stringify(value)}`;
17
+ }
18
+ return `!valuesEqual(${accessor}, ${JSON.stringify(value)})`;
33
19
  };
34
- const SCALAR_ITEM_TYPES = new Set(['string', 'number', 'integer', 'boolean', 'null']);
35
- /**
36
- * True when a schema's values are provably JSON scalars — its `type` is present
37
- * and every listed type is a primitive. Conservative: a `$ref`, a boolean/absent
38
- * schema, an `object`/`array` type, or a missing `type` all fail this test.
39
- */
20
+ const SCALAR_ITEM_TYPES = /* @__PURE__ */ new Set(["string", "number", "integer", "boolean", "null"]);
40
21
  const schemaIsScalarOnly = (schema) => {
41
- if (!isSchemaObject(schema))
42
- return false;
43
- const t = schema['type'];
44
- if (t === undefined)
45
- return false;
46
- const types = Array.isArray(t) ? t : [t];
47
- return types.length > 0 && types.every((x) => typeof x === 'string' && SCALAR_ITEM_TYPES.has(x));
22
+ if (!isSchemaObject(schema))
23
+ return false;
24
+ const t = schema["type"];
25
+ if (t === void 0)
26
+ return false;
27
+ const types = Array.isArray(t) ? t : [t];
28
+ return types.length > 0 && types.every((x) => typeof x === "string" && SCALAR_ITEM_TYPES.has(x));
48
29
  };
49
- /**
50
- * True when an array's elements can only be JSON scalars, so a `uniqueItems`
51
- * check can dedupe by the cheap `JSON.stringify` projection. When items may be
52
- * objects or arrays this returns false, and the check must instead compare
53
- * structurally (the `allUnique` runtime helper): `JSON.stringify` is key-order
54
- * sensitive and would treat `{ a: 1, b: 2 }` and `{ b: 2, a: 1 }` as distinct,
55
- * disagreeing with the interpreter's order-independent deep equality.
56
- */
57
30
  const arrayItemsAreScalarOnly = (schema) => {
58
- const prefix = schema['prefixItems'];
59
- if (Array.isArray(prefix)) {
60
- if (!prefix.every((p) => schemaIsScalarOnly(p)))
61
- return false;
62
- // Tuple tail: a closed tuple (`items`/`additionalItems: false`) has no tail;
63
- // otherwise the tail schema must itself be scalar-only.
64
- const tail = 'items' in schema ? schema['items'] : schema['additionalItems'];
65
- if (tail === false)
66
- return true;
67
- return schemaIsScalarOnly(tail);
68
- }
69
- return schemaIsScalarOnly(schema['items']);
31
+ const prefix = schema["prefixItems"];
32
+ if (Array.isArray(prefix)) {
33
+ if (!prefix.every((p) => schemaIsScalarOnly(p)))
34
+ return false;
35
+ const tail = "items" in schema ? schema["items"] : schema["additionalItems"];
36
+ if (tail === false)
37
+ return true;
38
+ return schemaIsScalarOnly(tail);
39
+ }
40
+ return schemaIsScalarOnly(schema["items"]);
70
41
  };
71
- /**
72
- * Generates the inline condition that is TRUE when a value is the wrong type.
73
- */
74
42
  const wrongTypeCondition = (accessor, type) => {
75
- switch (type) {
76
- case 'string':
77
- return `typeof ${accessor} !== 'string'`;
78
- case 'number':
79
- return `typeof ${accessor} !== 'number'`;
80
- case 'integer':
81
- return `typeof ${accessor} !== 'number' || !Number.isInteger(${accessor})`;
82
- case 'boolean':
83
- return `typeof ${accessor} !== 'boolean'`;
84
- case 'array':
85
- return `!Array.isArray(${accessor})`;
86
- case 'null':
87
- return `${accessor} !== null`;
88
- case 'object':
89
- return `typeof ${accessor} !== 'object' || ${accessor} === null || Array.isArray(${accessor})`;
90
- default:
91
- return '';
92
- }
43
+ switch (type) {
44
+ case "string":
45
+ return `typeof ${accessor} !== 'string'`;
46
+ case "number":
47
+ return `typeof ${accessor} !== 'number'`;
48
+ case "integer":
49
+ return `typeof ${accessor} !== 'number' || !Number.isInteger(${accessor})`;
50
+ case "boolean":
51
+ return `typeof ${accessor} !== 'boolean'`;
52
+ case "array":
53
+ return `!Array.isArray(${accessor})`;
54
+ case "null":
55
+ return `${accessor} !== null`;
56
+ case "object":
57
+ return `typeof ${accessor} !== 'object' || ${accessor} === null || Array.isArray(${accessor})`;
58
+ default:
59
+ return "";
60
+ }
93
61
  };
94
- /**
95
- * Returns the list of type names when a schema's `type` is an array (the JSON
96
- * Schema multi-type / nullable idiom, e.g. `["string","null"]`), else `null`.
97
- * `hasType` only recognises a *string* `type`, so without special handling a
98
- * multi-type schema slips through every branch and emits NO check — not even a
99
- * required-presence check. A multi-type is validated as the *disjunction* of its
100
- * per-type checks (the value must match at least one).
101
- */
102
62
  const getTypeArray = (schema) => {
103
- if (!isSchemaObject(schema) || !('type' in schema) || !Array.isArray(schema.type))
104
- return null;
105
- return schema.type;
63
+ if (!isSchemaObject(schema) || !("type" in schema) || !Array.isArray(schema.type))
64
+ return null;
65
+ return schema.type;
106
66
  };
107
- const createRootContext = () => ({ objVar: 'obj', pathPrefix: '${_path}', depth: 0, hoisted: [] });
108
- /**
109
- * Renders a schema-controlled property name as a static error-path segment.
110
- *
111
- * The name is appended to a backtick template-literal path (`` `${_path}/…` ``),
112
- * so two independent escapings apply. First the JSON Pointer escape (`~`→`~0`,
113
- * `/`→`~1`, `~` first) so a key containing `/` or `~` reads back unambiguously —
114
- * matching the paths the runtime-validators interpreter emits. Then a
115
- * template-literal escape of `` ` ``, `\`, and `$`, so a key like `` a`b `` or
116
- * `${x}` cannot terminate the literal (a build failure) or inject an
117
- * interpolation (a runtime `ReferenceError` / arbitrary expression).
118
- */
119
- const pointerSegment = (key) => key
120
- .replace(/~/g, '~0')
121
- .replace(/\//g, '~1')
122
- .replace(/[\\`$]/g, '\\$&');
123
- /**
124
- * Returns the `patternProperties` regex sources, or an empty array when the
125
- * schema declares none. The keys of `patternProperties` are the patterns.
126
- */
67
+ const createRootContext = () => ({ objVar: "obj", pathPrefix: "${_path}", depth: 0, hoisted: [] });
68
+ const pointerSegment = (key) => key.replace(/~/g, "~0").replace(/\//g, "~1").replace(/[\\`$]/g, "\\$&");
127
69
  const patternPropertySources = (schema) => {
128
- if (!isSchemaObject(schema) || !('patternProperties' in schema))
129
- return [];
130
- const patterns = schema.patternProperties;
131
- if (typeof patterns !== 'object' || patterns === null)
132
- return [];
133
- return Object.keys(patterns);
70
+ if (!isSchemaObject(schema) || !("patternProperties" in schema))
71
+ return [];
72
+ const patterns = schema.patternProperties;
73
+ if (typeof patterns !== "object" || patterns === null)
74
+ return [];
75
+ return Object.keys(patterns);
134
76
  };
135
- /**
136
- * Generates the unknown-key sweep for `additionalProperties: false`, mirroring
137
- * the interpreter's behaviour (same error message, one error per extra key).
138
- * The sweep uses `for...in` — the same allocation-free shape Ajv compiles to.
139
- * The per-key "is this declared" test comes from `unknownKeyCheck`, which
140
- * inlines `!==` comparisons for small key counts (faster than `Set.has` and
141
- * allocation-free) and hoists a known-keys `Set` only when the list is long.
142
- * When the schema also declares `patternProperties`, a key matching any pattern
143
- * is not "additional": the patterns are compiled once at module scope (the same
144
- * regex-caching the interpreter does) and a key survives the sweep if it is a
145
- * known key or matches any pattern.
146
- */
147
77
  const generateStrictKeyChecks = (schema, ctx) => {
148
- if (!isSchemaObject(schema))
149
- return [];
150
- if (!hasAdditionalProperties(schema) || schema.additionalProperties !== false)
151
- return [];
152
- const known = Object.keys(hasProperties(schema) ? schema.properties : {});
153
- const d = ctx.depth;
154
- const check = unknownKeyCheck(known, `_knownKeys${ctx.hoisted.length}`);
155
- ctx.hoisted.push(...check.declarations);
156
- // A key that matches any `patternProperties` regex is allowed, so only keys
157
- // outside both the known keys and every pattern count as additional.
158
- const patterns = patternPropertySources(schema);
159
- let patternGuard = '';
160
- if (patterns.length > 0) {
161
- const patternsName = `_patterns${ctx.hoisted.length}`;
162
- ctx.hoisted.push(`const ${patternsName} = [${patterns.map((p) => `new RegExp(${JSON.stringify(p)})`).join(', ')}]`);
163
- patternGuard = ` && !${patternsName}.some((re) => re.test(_key${d}))`;
164
- }
165
- return [
166
- ` for (const _key${d} in ${ctx.objVar}) {`,
167
- ` if (${check.isUnknown(`_key${d}`)}${patternGuard}) {`,
168
- ` errors.push({ message: 'must NOT have additional properties', path: \`${ctx.pathPrefix}/\${_key${d}}\` })`,
169
- ` }`,
170
- ` }`,
171
- ];
78
+ if (!isSchemaObject(schema))
79
+ return [];
80
+ if (!hasAdditionalProperties(schema) || schema.additionalProperties !== false)
81
+ return [];
82
+ const known = Object.keys(hasProperties(schema) ? schema.properties : {});
83
+ const d = ctx.depth;
84
+ const check = unknownKeyCheck(known, `_knownKeys${ctx.hoisted.length}`);
85
+ ctx.hoisted.push(...check.declarations);
86
+ const patterns = patternPropertySources(schema);
87
+ let patternGuard = "";
88
+ if (patterns.length > 0) {
89
+ const patternsName = `_patterns${ctx.hoisted.length}`;
90
+ ctx.hoisted.push(`const ${patternsName} = [${patterns.map((p) => `new RegExp(${JSON.stringify(p)})`).join(", ")}]`);
91
+ patternGuard = ` && !${patternsName}.some((re) => re.test(_key${d}))`;
92
+ }
93
+ return [
94
+ ` for (const _key${d} in ${ctx.objVar}) {`,
95
+ ` if (${check.isUnknown(`_key${d}`)}${patternGuard}) {`,
96
+ ` errors.push({ message: 'must NOT have additional properties', path: \`${ctx.pathPrefix}/\${_key${d}}\` })`,
97
+ ` }`,
98
+ ` }`
99
+ ];
172
100
  };
173
- /**
174
- * Emits presence checks for `required` keys that have no `properties` entry.
175
- * Keys present in `properties` get their missing-property check from
176
- * {@link generatePropertyChecks}; a required key with no schema of its own would
177
- * otherwise go unchecked, so its presence is enforced here to match the
178
- * interpreter and Ajv.
179
- */
180
101
  const generateMissingRequiredChecks = (schema, ctx) => {
181
- if (!isSchemaObject(schema) || !hasRequired(schema))
182
- return [];
183
- const props = hasProperties(schema) ? schema.properties : {};
184
- const parentPath = ctx.depth === 0 ? '_path' : `\`${ctx.pathPrefix}\``;
185
- const lines = [];
186
- for (const key of schema.required) {
187
- if (Object.hasOwn(props, key))
188
- continue;
189
- lines.push(` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`);
190
- lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
191
- lines.push(` }`);
192
- }
193
- return lines;
102
+ if (!isSchemaObject(schema) || !hasRequired(schema))
103
+ return [];
104
+ const props = hasProperties(schema) ? schema.properties : {};
105
+ const parentPath = ctx.depth === 0 ? "_path" : `\`${ctx.pathPrefix}\``;
106
+ const lines = [];
107
+ for (const key of schema.required) {
108
+ if (Object.hasOwn(props, key))
109
+ continue;
110
+ lines.push(` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`);
111
+ lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
112
+ lines.push(` }`);
113
+ }
114
+ return lines;
194
115
  };
195
- /**
196
- * Generates validation lines for a single property in an object schema.
197
- * Handles $ref delegation, enum checks, type checks, string/number constraints,
198
- * and recursion into inline nested objects.
199
- */
200
116
  const generatePropertyChecks = (key, propSchema, isRequired, suffix, ctx) => {
201
- if (!isSchemaObject(propSchema)) {
202
- // A boolean `true` (accept-anything) schema carries no shape checks, but a
203
- // required key must still be present. `false` never validates a present value,
204
- // which the strict-key / additionalProperties path handles; here we only need
205
- // to enforce presence for `true`.
206
- if (isRequired && propSchema === true) {
207
- const parentPath = ctx.depth === 0 ? '_path' : `\`${ctx.pathPrefix}\``;
208
- return [
209
- ` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`,
210
- ` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`,
211
- ` }`,
212
- ];
213
- }
214
- return [];
215
- }
216
- const raw = `${ctx.objVar}[${JSON.stringify(key)}]`;
217
- const path = `\`${ctx.pathPrefix}/${pointerSegment(key)}\``;
218
- // Missing-property errors report at the parent object's path. At the root
219
- // that is the `_path` parameter itself; inside nested objects it is the
220
- // parent's accumulated static path.
221
- const parentPath = ctx.depth === 0 ? '_path' : `\`${ctx.pathPrefix}\``;
222
- const lines = [];
223
- // $ref — delegate to the imported validator. Per 2020-12, sibling keywords
224
- // alongside `$ref` still apply to the same value, so any constraint/combinator
225
- // siblings (e.g. `{ $ref, minLength: 5 }`) run after the delegation. A bare
226
- // `{ $ref }` produces no siblings, leaving the output unchanged.
227
- if (hasRef(propSchema)) {
228
- const ref = propSchema.$ref;
229
- const vName = validatorName(refToName(ref, suffix));
230
- const siblings = [
231
- ...generateConstraintChecks(key, raw, path, propSchema, suffix, ctx),
232
- ...generateCombinatorChecks(key, raw, path, propSchema, suffix, ctx),
233
- ];
234
- const delegate = [
235
- ` const _r = ${vName}(${raw}, ${path})`,
236
- ` if (_r !== true) errors.push(..._r.errors)`,
237
- ...siblings,
238
- ];
239
- if (isRequired) {
240
- lines.push(` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`);
241
- lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
242
- lines.push(` } else {`);
243
- lines.push(...delegate);
244
- lines.push(` }`);
245
- }
246
- else {
247
- lines.push(` if (${raw} !== undefined) {`);
248
- lines.push(...delegate);
249
- lines.push(` }`);
250
- }
251
- return lines;
252
- }
253
- // x-mjst instanceOf (e.g. Date) — value must be an instance of the class
254
- const instanceOf = getMjstInstanceOf(propSchema);
255
- if (instanceOf) {
256
- if (isRequired) {
257
- lines.push(` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`);
258
- lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
259
- lines.push(` } else if (!(${raw} instanceof ${instanceOf})) {`);
260
- lines.push(` errors.push({ message: 'must be ${instanceOf}', path: ${path} })`);
261
- lines.push(` }`);
262
- }
263
- else {
264
- lines.push(` if (${raw} !== undefined && !(${raw} instanceof ${instanceOf})) {`);
265
- lines.push(` errors.push({ message: 'must be ${instanceOf}', path: ${path} })`);
266
- lines.push(` }`);
267
- }
268
- return lines;
269
- }
270
- // x-mjst primitive (e.g. bigint) — value must satisfy a typeof check
271
- const primitive = getMjstPrimitive(propSchema);
272
- if (primitive) {
273
- if (isRequired) {
274
- lines.push(` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`);
275
- lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
276
- lines.push(` } else if (typeof ${raw} !== "${primitive}") {`);
277
- lines.push(` errors.push({ message: 'must be ${primitive}', path: ${path} })`);
278
- lines.push(` }`);
279
- }
280
- else {
281
- lines.push(` if (${raw} !== undefined && typeof ${raw} !== "${primitive}") {`);
282
- lines.push(` errors.push({ message: 'must be ${primitive}', path: ${path} })`);
283
- lines.push(` }`);
284
- }
285
- return lines;
286
- }
287
- // const — value must equal the fixed value exactly
288
- if (hasConst(propSchema)) {
289
- const mismatch = constMismatchCondition(raw, propSchema.const);
290
- const msg = JSON.stringify(`must be ${JSON.stringify(propSchema.const)}`);
291
- if (isRequired) {
292
- lines.push(` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`);
293
- lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
294
- lines.push(` } else if (${mismatch}) {`);
295
- lines.push(` errors.push({ message: ${msg}, path: ${path} })`);
296
- lines.push(` }`);
297
- }
298
- else {
299
- lines.push(` if (${raw} !== undefined && ${mismatch}) {`);
300
- lines.push(` errors.push({ message: ${msg}, path: ${path} })`);
301
- lines.push(` }`);
302
- }
303
- return lines;
304
- }
305
- // enum
306
- if (hasEnum(propSchema)) {
307
- const allowed = JSON.stringify(propSchema.enum);
308
- const label = propSchema.enum.map((v) => JSON.stringify(v)).join(', ');
309
- if (isRequired) {
310
- lines.push(` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`);
311
- lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
312
- lines.push(` } else if (!(${allowed} as unknown[]).includes(${raw})) {`);
313
- lines.push(` errors.push({ message: ${JSON.stringify(`must be one of: ${label}`)}, path: ${path} })`);
314
- lines.push(` }`);
315
- }
316
- else {
317
- lines.push(` if (${raw} !== undefined && !(${allowed} as unknown[]).includes(${raw})) {`);
318
- lines.push(` errors.push({ message: ${JSON.stringify(`must be one of: ${label}`)}, path: ${path} })`);
319
- lines.push(` }`);
320
- }
321
- return lines;
322
- }
323
- // Multi-type / nullable property (array `type`, e.g. `["string","null"]`).
324
- // `hasType` is false for an array `type`, so without this the property emits no
325
- // check at all. The value is valid when it matches ANY listed type, i.e. an
326
- // error is reported only when it is the wrong type for EVERY listed type; the
327
- // required-presence check is still emitted so a missing required prop fails.
328
- const typeArray = getTypeArray(propSchema);
329
- if (typeArray) {
330
- const allWrong = typeArray
331
- .map((t) => wrongTypeCondition(raw, t))
332
- .filter((c) => c !== '')
333
- .map((c) => `(${c})`)
334
- .join(' && ');
335
- const label = typeArray.map((t) => typeofString(t)).join(' or ');
336
- if (isRequired) {
337
- lines.push(` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`);
338
- lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
339
- if (allWrong) {
340
- lines.push(` } else if (${allWrong}) {`);
341
- lines.push(` errors.push({ message: ${JSON.stringify(`must be ${label}`)}, path: ${path} })`);
342
- }
343
- lines.push(` }`);
344
- }
345
- else if (allWrong) {
346
- lines.push(` if (${raw} !== undefined && (${allWrong})) {`);
347
- lines.push(` errors.push({ message: ${JSON.stringify(`must be ${label}`)}, path: ${path} })`);
348
- lines.push(` }`);
349
- }
350
- // Any sibling value constraints (e.g. `minLength` on a `["string","null"]`)
351
- // still apply — each carries its own runtime-type guard, so it is a no-op for
352
- // the values it does not target.
353
- lines.push(...generateConstraintChecks(key, raw, path, propSchema, suffix, ctx));
354
- return lines;
117
+ if (!isSchemaObject(propSchema)) {
118
+ if (isRequired && propSchema === true) {
119
+ const parentPath2 = ctx.depth === 0 ? "_path" : `\`${ctx.pathPrefix}\``;
120
+ return [
121
+ ` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`,
122
+ ` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath2} })`,
123
+ ` }`
124
+ ];
125
+ }
126
+ return [];
127
+ }
128
+ const raw = `${ctx.objVar}[${JSON.stringify(key)}]`;
129
+ const path = `\`${ctx.pathPrefix}/${pointerSegment(key)}\``;
130
+ const parentPath = ctx.depth === 0 ? "_path" : `\`${ctx.pathPrefix}\``;
131
+ const lines = [];
132
+ if (hasRef(propSchema)) {
133
+ const ref = propSchema.$ref;
134
+ const vName = validatorName(refToName(ref, suffix));
135
+ const siblings = [
136
+ ...generateConstraintChecks(key, raw, path, propSchema, suffix, ctx),
137
+ ...generateCombinatorChecks(key, raw, path, propSchema, suffix, ctx)
138
+ ];
139
+ const delegate = [
140
+ ` const _r = ${vName}(${raw}, ${path})`,
141
+ ` if (_r !== true) errors.push(..._r.errors)`,
142
+ ...siblings
143
+ ];
144
+ if (isRequired) {
145
+ lines.push(` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`);
146
+ lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
147
+ lines.push(` } else {`);
148
+ lines.push(...delegate);
149
+ lines.push(` }`);
150
+ } else {
151
+ lines.push(` if (${raw} !== undefined) {`);
152
+ lines.push(...delegate);
153
+ lines.push(` }`);
355
154
  }
356
- // typed property
357
- if (hasType(propSchema)) {
358
- const t = propSchema.type;
359
- const wrongType = wrongTypeCondition(raw, t);
360
- const typLabel = typeofString(t);
361
- if (isRequired) {
362
- lines.push(` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`);
363
- lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
364
- if (wrongType) {
365
- lines.push(` } else if (${wrongType}) {`);
366
- lines.push(` errors.push({ message: 'must be ${typLabel}', path: ${path} })`);
367
- }
368
- lines.push(` }`);
369
- }
370
- else if (wrongType) {
371
- lines.push(` if (${raw} !== undefined && (${wrongType})) {`);
372
- lines.push(` errors.push({ message: 'must be ${typLabel}', path: ${path} })`);
373
- lines.push(` }`);
374
- }
375
- lines.push(...generateConstraintChecks(key, raw, path, propSchema, suffix, ctx));
155
+ return lines;
156
+ }
157
+ const instanceOf = getMjstInstanceOf(propSchema);
158
+ if (instanceOf) {
159
+ if (isRequired) {
160
+ lines.push(` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`);
161
+ lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
162
+ lines.push(` } else if (!(${raw} instanceof ${instanceOf})) {`);
163
+ lines.push(` errors.push({ message: 'must be ${instanceOf}', path: ${path} })`);
164
+ lines.push(` }`);
165
+ } else {
166
+ lines.push(` if (${raw} !== undefined && !(${raw} instanceof ${instanceOf})) {`);
167
+ lines.push(` errors.push({ message: 'must be ${instanceOf}', path: ${path} })`);
168
+ lines.push(` }`);
376
169
  }
377
- // Keywords that can sit alongside or instead of `type`: combinators
378
- // (`allOf`/`anyOf`/`oneOf`/`not`/`if`) for any schema, plus the constraint
379
- // checks for a *type-less* schema (e.g. a bare `{ required: [...] }` or
380
- // `{ minItems: 2 }` property). A typed schema already ran its constraints in
381
- // the `hasType` branch above, so it only needs the combinators here.
382
- const extraLines = hasType(propSchema)
383
- ? generateCombinatorChecks(key, raw, path, propSchema, suffix, ctx)
384
- : [
385
- ...generateConstraintChecks(key, raw, path, propSchema, suffix, ctx),
386
- ...generateCombinatorChecks(key, raw, path, propSchema, suffix, ctx),
387
- ];
388
- if (hasType(propSchema)) {
389
- // Presence was already enforced in the `hasType` branch; only wrap the
390
- // combinator siblings so they run when the value is present.
391
- if (extraLines.length > 0) {
392
- lines.push(` if (${raw} !== undefined) {`);
393
- lines.push(...extraLines);
394
- lines.push(` }`);
395
- }
170
+ return lines;
171
+ }
172
+ const primitive = getMjstPrimitive(propSchema);
173
+ if (primitive) {
174
+ if (isRequired) {
175
+ lines.push(` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`);
176
+ lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
177
+ lines.push(` } else if (typeof ${raw} !== "${primitive}") {`);
178
+ lines.push(` errors.push({ message: 'must be ${primitive}', path: ${path} })`);
179
+ lines.push(` }`);
180
+ } else {
181
+ lines.push(` if (${raw} !== undefined && typeof ${raw} !== "${primitive}") {`);
182
+ lines.push(` errors.push({ message: 'must be ${primitive}', path: ${path} })`);
183
+ lines.push(` }`);
396
184
  }
397
- else if (isRequired) {
398
- // Type-less required property. Presence must be enforced even when the schema
399
- // contributes no other checks (e.g. `{}` — an accept-anything schema), so a
400
- // missing required key is still an error. Any extra checks run in the `else`.
401
- lines.push(` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`);
402
- lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
403
- if (extraLines.length > 0) {
404
- lines.push(` } else {`);
405
- lines.push(...extraLines);
406
- }
407
- lines.push(` }`);
185
+ return lines;
186
+ }
187
+ if (hasConst(propSchema)) {
188
+ const mismatch = constMismatchCondition(raw, propSchema.const);
189
+ const msg = JSON.stringify(`must be ${JSON.stringify(propSchema.const)}`);
190
+ if (isRequired) {
191
+ lines.push(` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`);
192
+ lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
193
+ lines.push(` } else if (${mismatch}) {`);
194
+ lines.push(` errors.push({ message: ${msg}, path: ${path} })`);
195
+ lines.push(` }`);
196
+ } else {
197
+ lines.push(` if (${raw} !== undefined && ${mismatch}) {`);
198
+ lines.push(` errors.push({ message: ${msg}, path: ${path} })`);
199
+ lines.push(` }`);
408
200
  }
409
- else if (extraLines.length > 0) {
410
- // Type-less optional property: run any checks only when the value is present.
411
- lines.push(` if (${raw} !== undefined) {`);
412
- lines.push(...extraLines);
413
- lines.push(` }`);
201
+ return lines;
202
+ }
203
+ if (hasEnum(propSchema)) {
204
+ const allowed = JSON.stringify(propSchema.enum);
205
+ const label = propSchema.enum.map((v) => JSON.stringify(v)).join(", ");
206
+ if (isRequired) {
207
+ lines.push(` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`);
208
+ lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
209
+ lines.push(` } else if (!(${allowed} as unknown[]).includes(${raw})) {`);
210
+ lines.push(` errors.push({ message: ${JSON.stringify(`must be one of: ${label}`)}, path: ${path} })`);
211
+ lines.push(` }`);
212
+ } else {
213
+ lines.push(` if (${raw} !== undefined && !(${allowed} as unknown[]).includes(${raw})) {`);
214
+ lines.push(` errors.push({ message: ${JSON.stringify(`must be one of: ${label}`)}, path: ${path} })`);
215
+ lines.push(` }`);
414
216
  }
415
217
  return lines;
218
+ }
219
+ const typeArray = getTypeArray(propSchema);
220
+ if (typeArray) {
221
+ const allWrong = typeArray.map((t) => wrongTypeCondition(raw, t)).filter((c) => c !== "").map((c) => `(${c})`).join(" && ");
222
+ const label = typeArray.map((t) => typeofString(t)).join(" or ");
223
+ if (isRequired) {
224
+ lines.push(` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`);
225
+ lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
226
+ if (allWrong) {
227
+ lines.push(` } else if (${allWrong}) {`);
228
+ lines.push(` errors.push({ message: ${JSON.stringify(`must be ${label}`)}, path: ${path} })`);
229
+ }
230
+ lines.push(` }`);
231
+ } else if (allWrong) {
232
+ lines.push(` if (${raw} !== undefined && (${allWrong})) {`);
233
+ lines.push(` errors.push({ message: ${JSON.stringify(`must be ${label}`)}, path: ${path} })`);
234
+ lines.push(` }`);
235
+ }
236
+ lines.push(...generateConstraintChecks(key, raw, path, propSchema, suffix, ctx));
237
+ return lines;
238
+ }
239
+ if (hasType(propSchema)) {
240
+ const t = propSchema.type;
241
+ const wrongType = wrongTypeCondition(raw, t);
242
+ const typLabel = typeofString(t);
243
+ if (isRequired) {
244
+ lines.push(` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`);
245
+ lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
246
+ if (wrongType) {
247
+ lines.push(` } else if (${wrongType}) {`);
248
+ lines.push(` errors.push({ message: 'must be ${typLabel}', path: ${path} })`);
249
+ }
250
+ lines.push(` }`);
251
+ } else if (wrongType) {
252
+ lines.push(` if (${raw} !== undefined && (${wrongType})) {`);
253
+ lines.push(` errors.push({ message: 'must be ${typLabel}', path: ${path} })`);
254
+ lines.push(` }`);
255
+ }
256
+ lines.push(...generateConstraintChecks(key, raw, path, propSchema, suffix, ctx));
257
+ }
258
+ const extraLines = hasType(propSchema) ? generateCombinatorChecks(key, raw, path, propSchema, suffix, ctx) : [
259
+ ...generateConstraintChecks(key, raw, path, propSchema, suffix, ctx),
260
+ ...generateCombinatorChecks(key, raw, path, propSchema, suffix, ctx)
261
+ ];
262
+ if (hasType(propSchema)) {
263
+ if (extraLines.length > 0) {
264
+ lines.push(` if (${raw} !== undefined) {`);
265
+ lines.push(...extraLines);
266
+ lines.push(` }`);
267
+ }
268
+ } else if (isRequired) {
269
+ lines.push(` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`);
270
+ lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
271
+ if (extraLines.length > 0) {
272
+ lines.push(` } else {`);
273
+ lines.push(...extraLines);
274
+ }
275
+ lines.push(` }`);
276
+ } else if (extraLines.length > 0) {
277
+ lines.push(` if (${raw} !== undefined) {`);
278
+ lines.push(...extraLines);
279
+ lines.push(` }`);
280
+ }
281
+ return lines;
416
282
  };
417
- /**
418
- * Emits the value-shape constraints for a typed value: string (pattern,
419
- * min/maxLength), number/integer (bounds, multipleOf), typed/`$ref` array items,
420
- * and recursion into an inline nested object. Shared by the named-property path
421
- * ({@link generatePropertyChecks}) and the dynamic-key path
422
- * ({@link generateValueChecks}) so both enforce identical rules. `raw` and `path`
423
- * are arbitrary expressions, so the same logic serves a static `obj.key` and a
424
- * `patternProperties` / `additionalProperties` value read at a runtime key.
425
- */
426
283
  const generateConstraintChecks = (key, raw, path, propSchema, suffix, ctx) => {
427
- if (!isSchemaObject(propSchema))
428
- return [];
429
- const sp = propSchema;
430
- const lines = [];
431
- // Each block is gated on the *presence of its keywords*, not a declared `type`,
432
- // and every emitted check carries its own runtime-type guard (`typeof` /
433
- // `Array.isArray`). So a type-less schema (e.g. an `allOf` / `anyOf` / `not`
434
- // branch that is just `{ required: [...] }` or `{ minItems: 2 }`) is validated
435
- // against the value's runtime type, matching the interpreter.
436
- // String constraints
437
- if (hasPattern(propSchema) || hasMinLength(propSchema) || hasMaxLength(propSchema)) {
438
- if (hasPattern(propSchema)) {
439
- const re = escapeRegexPattern(propSchema.pattern);
440
- const msg = JSON.stringify(`must match pattern ${propSchema.pattern}`);
441
- lines.push(` if (typeof ${raw} === 'string' && !/${re}/.test(${raw})) {`);
442
- lines.push(` errors.push({ message: ${msg}, path: ${path} })`);
443
- lines.push(` }`);
444
- }
445
- if (hasMinLength(propSchema)) {
446
- lines.push(` if (typeof ${raw} === 'string' && ${raw}.length < ${propSchema.minLength}) {`);
447
- lines.push(` errors.push({ message: 'must have at least ${propSchema.minLength} characters', path: ${path} })`);
448
- lines.push(` }`);
449
- }
450
- if (hasMaxLength(propSchema)) {
451
- lines.push(` if (typeof ${raw} === 'string' && ${raw}.length > ${propSchema.maxLength}) {`);
452
- lines.push(` errors.push({ message: 'must have at most ${propSchema.maxLength} characters', path: ${path} })`);
453
- lines.push(` }`);
454
- }
455
- }
456
- // Number constraints
457
- if (hasMinimum(propSchema) ||
458
- hasMaximum(propSchema) ||
459
- hasExclusiveMinimum(propSchema) ||
460
- hasExclusiveMaximum(propSchema) ||
461
- hasMultipleOf(propSchema)) {
462
- if (hasMinimum(propSchema)) {
463
- // Draft-04 `exclusiveMinimum: true` makes the paired `minimum` strict.
464
- const strict = hasStrictExclusiveMinimum(propSchema);
465
- const op = strict ? '<=' : '<';
466
- const rel = strict ? '>' : '>=';
467
- lines.push(` if (typeof ${raw} === 'number' && ${raw} ${op} ${propSchema.minimum}) {`);
468
- lines.push(` errors.push({ message: 'must be ${rel} ${propSchema.minimum}', path: ${path} })`);
469
- lines.push(` }`);
470
- }
471
- if (hasMaximum(propSchema)) {
472
- const strict = hasStrictExclusiveMaximum(propSchema);
473
- const op = strict ? '>=' : '>';
474
- const rel = strict ? '<' : '<=';
475
- lines.push(` if (typeof ${raw} === 'number' && ${raw} ${op} ${propSchema.maximum}) {`);
476
- lines.push(` errors.push({ message: 'must be ${rel} ${propSchema.maximum}', path: ${path} })`);
477
- lines.push(` }`);
478
- }
479
- if (hasExclusiveMinimum(propSchema)) {
480
- lines.push(` if (typeof ${raw} === 'number' && ${raw} <= ${propSchema.exclusiveMinimum}) {`);
481
- lines.push(` errors.push({ message: 'must be > ${propSchema.exclusiveMinimum}', path: ${path} })`);
482
- lines.push(` }`);
483
- }
484
- if (hasExclusiveMaximum(propSchema)) {
485
- lines.push(` if (typeof ${raw} === 'number' && ${raw} >= ${propSchema.exclusiveMaximum}) {`);
486
- lines.push(` errors.push({ message: 'must be < ${propSchema.exclusiveMaximum}', path: ${path} })`);
487
- lines.push(` }`);
488
- }
489
- if (hasMultipleOf(propSchema)) {
490
- lines.push(` if (typeof ${raw} === 'number' && ${multipleOfFailExpr(raw, propSchema.multipleOf)}) {`);
491
- lines.push(` errors.push({ message: 'must be a multiple of ${propSchema.multipleOf}', path: ${path} })`);
492
- lines.push(` }`);
493
- }
494
- }
495
- // Array items. `$ref` items delegate to the referenced validator. Any other
496
- // item subschema is validated in full — matching the interpreter — but wrapped
497
- // in a per-item boolean fast-check (`booleanLeafExpr`): a valid item passes the
498
- // flat expression and skips the error-collecting body entirely, so the common
499
- // valid case stays allocation-free (the same hot/cold split the top-level
500
- // validator uses). This keeps array-heavy throughput close to a bare type check
501
- // while still fully validating every item. The loop variables carry the nesting
502
- // depth so item loops can nest (array-of-arrays) without colliding.
503
- if (hasItems(propSchema)) {
504
- const itemSchema = propSchema.items;
505
- const iv = `_i${ctx.depth}`;
506
- const itemPath = `\`${path.slice(1, -1)}/\${${iv}}\``;
507
- if (hasRef(itemSchema)) {
508
- const vName = validatorName(refToName(itemSchema.$ref, suffix));
509
- lines.push(` if (Array.isArray(${raw})) {`);
510
- lines.push(` for (let ${iv} = 0; ${iv} < ${raw}.length; ${iv}++) {`);
511
- lines.push(` const _ir = ${vName}(${raw}[${iv}], ${itemPath})`);
512
- lines.push(` if (_ir !== true) errors.push(..._ir.errors)`);
513
- lines.push(` }`);
514
- lines.push(` }`);
515
- }
516
- else if (isSchemaObject(itemSchema)) {
517
- const itemVar = `_item${ctx.depth}`;
518
- const detail = generateValueChecks('', itemVar, itemPath, itemSchema, suffix, ctx, true);
519
- if (detail.length > 0) {
520
- lines.push(` if (Array.isArray(${raw})) {`);
521
- lines.push(` for (let ${iv} = 0; ${iv} < ${raw}.length; ${iv}++) {`);
522
- lines.push(` const ${itemVar} = ${raw}[${iv}]`);
523
- lines.push(...detail.map((l) => ` ${l}`));
524
- lines.push(` }`);
525
- lines.push(` }`);
526
- }
527
- }
528
- }
529
- // Array length / uniqueness. `uniqueItems` dedupes scalar items by a cheap
530
- // `JSON.stringify` projection (exact for primitives, what the type guard also
531
- // uses), but falls back to the structural `allUnique` helper when items may be
532
- // objects/arrays `JSON.stringify` is key-order sensitive and would disagree
533
- // with the interpreter's order-independent deep equality.
534
- if (hasMinItems(propSchema) ||
535
- hasMaxItems(propSchema) ||
536
- (hasUniqueItems(propSchema) && propSchema.uniqueItems === true) ||
537
- isSchemaObject(sp['contains']) ||
538
- Array.isArray(sp['prefixItems']) ||
539
- (sp['items'] === false && !Array.isArray(sp['prefixItems']))) {
540
- // `items: false` with no `prefixItems` forbids every element, so the array
541
- // must be empty. (With `prefixItems`, the tuple block below caps the length
542
- // instead.) Without this the constraint was silently ignored.
543
- if (sp['items'] === false && !Array.isArray(sp['prefixItems'])) {
544
- lines.push(` if (Array.isArray(${raw}) && ${raw}.length > 0) {`);
545
- lines.push(` errors.push({ message: 'must NOT have more than 0 items', path: ${path} })`);
546
- lines.push(` }`);
547
- }
548
- if (hasMinItems(propSchema)) {
549
- lines.push(` if (Array.isArray(${raw}) && ${raw}.length < ${propSchema.minItems}) {`);
550
- lines.push(` errors.push({ message: 'must have at least ${propSchema.minItems} items', path: ${path} })`);
551
- lines.push(` }`);
552
- }
553
- if (hasMaxItems(propSchema)) {
554
- lines.push(` if (Array.isArray(${raw}) && ${raw}.length > ${propSchema.maxItems}) {`);
555
- lines.push(` errors.push({ message: 'must have at most ${propSchema.maxItems} items', path: ${path} })`);
556
- lines.push(` }`);
557
- }
558
- if (hasUniqueItems(propSchema) && propSchema.uniqueItems === true) {
559
- const dupCond = arrayItemsAreScalarOnly(sp)
560
- ? `new Set((${raw} as unknown[]).map((_u) => JSON.stringify(_u))).size !== ${raw}.length`
561
- : `!allUnique(${raw} as unknown[])`;
562
- lines.push(` if (Array.isArray(${raw}) && ${dupCond}) {`);
563
- lines.push(` errors.push({ message: 'must NOT have duplicate items', path: ${path} })`);
564
- lines.push(` }`);
565
- }
566
- // `contains` — at least `minContains` (default 1) and at most `maxContains`
567
- // items must match the subschema. `minContains: 0` makes any array (even
568
- // empty) satisfy the lower bound.
569
- if (isSchemaObject(sp['contains'])) {
570
- const min = typeof sp['minContains'] === 'number' ? sp['minContains'] : 1;
571
- const max = typeof sp['maxContains'] === 'number' ? sp['maxContains'] : undefined;
572
- const matchExpr = generateMatchesExpr('_c', sp['contains'], suffix, ctx);
573
- const bound = max !== undefined ? `_cn < ${min} || _cn > ${max}` : `_cn < ${min}`;
574
- lines.push(` if (Array.isArray(${raw})) {`);
575
- lines.push(` const _cn = (${raw} as unknown[]).filter((_c) => ${matchExpr}).length`);
576
- lines.push(` if (${bound}) {`);
577
- lines.push(` errors.push({ message: 'array does not contain the required matching items', path: ${path} })`);
578
- lines.push(` }`);
579
- lines.push(` }`);
580
- }
581
- // Tuple `prefixItems` — each position validated against its own subschema; a
582
- // sibling `items: false` (or draft `additionalItems: false`) caps the length.
583
- const prefix = sp['prefixItems'];
584
- if (Array.isArray(prefix)) {
585
- lines.push(` if (Array.isArray(${raw})) {`);
586
- for (let i = 0; i < prefix.length; i++) {
587
- const itemChecks = generateValueChecks('', `${raw}[${i}]`, `\`${path.slice(1, -1)}/${i}\``, prefix[i], suffix, ctx);
588
- if (itemChecks.length > 0) {
589
- lines.push(` if (${raw}.length > ${i}) {`);
590
- lines.push(...itemChecks.map((l) => ` ${l}`));
591
- lines.push(` }`);
592
- }
593
- }
594
- if (sp['items'] === false || sp['additionalItems'] === false) {
595
- lines.push(` if (${raw}.length > ${prefix.length}) {`);
596
- lines.push(` errors.push({ message: 'must NOT have more than ${prefix.length} items', path: ${path} })`);
597
- lines.push(` }`);
598
- }
599
- lines.push(` }`);
284
+ if (!isSchemaObject(propSchema))
285
+ return [];
286
+ const sp = propSchema;
287
+ const lines = [];
288
+ if (hasPattern(propSchema) || hasMinLength(propSchema) || hasMaxLength(propSchema)) {
289
+ if (hasPattern(propSchema)) {
290
+ const re = escapeRegexPattern(propSchema.pattern);
291
+ const msg = JSON.stringify(`must match pattern ${propSchema.pattern}`);
292
+ lines.push(` if (typeof ${raw} === 'string' && !/${re}/.test(${raw})) {`);
293
+ lines.push(` errors.push({ message: ${msg}, path: ${path} })`);
294
+ lines.push(` }`);
295
+ }
296
+ if (hasMinLength(propSchema)) {
297
+ lines.push(` if (typeof ${raw} === 'string' && ${raw}.length < ${propSchema.minLength}) {`);
298
+ lines.push(` errors.push({ message: 'must have at least ${propSchema.minLength} characters', path: ${path} })`);
299
+ lines.push(` }`);
300
+ }
301
+ if (hasMaxLength(propSchema)) {
302
+ lines.push(` if (typeof ${raw} === 'string' && ${raw}.length > ${propSchema.maxLength}) {`);
303
+ lines.push(` errors.push({ message: 'must have at most ${propSchema.maxLength} characters', path: ${path} })`);
304
+ lines.push(` }`);
305
+ }
306
+ }
307
+ if (hasMinimum(propSchema) || hasMaximum(propSchema) || hasExclusiveMinimum(propSchema) || hasExclusiveMaximum(propSchema) || hasMultipleOf(propSchema)) {
308
+ if (hasMinimum(propSchema)) {
309
+ const strict = hasStrictExclusiveMinimum(propSchema);
310
+ const op = strict ? "<=" : "<";
311
+ const rel = strict ? ">" : ">=";
312
+ lines.push(` if (typeof ${raw} === 'number' && ${raw} ${op} ${propSchema.minimum}) {`);
313
+ lines.push(` errors.push({ message: 'must be ${rel} ${propSchema.minimum}', path: ${path} })`);
314
+ lines.push(` }`);
315
+ }
316
+ if (hasMaximum(propSchema)) {
317
+ const strict = hasStrictExclusiveMaximum(propSchema);
318
+ const op = strict ? ">=" : ">";
319
+ const rel = strict ? "<" : "<=";
320
+ lines.push(` if (typeof ${raw} === 'number' && ${raw} ${op} ${propSchema.maximum}) {`);
321
+ lines.push(` errors.push({ message: 'must be ${rel} ${propSchema.maximum}', path: ${path} })`);
322
+ lines.push(` }`);
323
+ }
324
+ if (hasExclusiveMinimum(propSchema)) {
325
+ lines.push(` if (typeof ${raw} === 'number' && ${raw} <= ${propSchema.exclusiveMinimum}) {`);
326
+ lines.push(` errors.push({ message: 'must be > ${propSchema.exclusiveMinimum}', path: ${path} })`);
327
+ lines.push(` }`);
328
+ }
329
+ if (hasExclusiveMaximum(propSchema)) {
330
+ lines.push(` if (typeof ${raw} === 'number' && ${raw} >= ${propSchema.exclusiveMaximum}) {`);
331
+ lines.push(` errors.push({ message: 'must be < ${propSchema.exclusiveMaximum}', path: ${path} })`);
332
+ lines.push(` }`);
333
+ }
334
+ if (hasMultipleOf(propSchema)) {
335
+ lines.push(` if (typeof ${raw} === 'number' && ${multipleOfFailExpr(raw, propSchema.multipleOf)}) {`);
336
+ lines.push(` errors.push({ message: 'must be a multiple of ${propSchema.multipleOf}', path: ${path} })`);
337
+ lines.push(` }`);
338
+ }
339
+ }
340
+ if (hasItems(propSchema)) {
341
+ const itemSchema = propSchema.items;
342
+ const iv = `_i${ctx.depth}`;
343
+ const itemPath = `\`${path.slice(1, -1)}/\${${iv}}\``;
344
+ if (hasRef(itemSchema)) {
345
+ const vName = validatorName(refToName(itemSchema.$ref, suffix));
346
+ lines.push(` if (Array.isArray(${raw})) {`);
347
+ lines.push(` for (let ${iv} = 0; ${iv} < ${raw}.length; ${iv}++) {`);
348
+ lines.push(` const _ir = ${vName}(${raw}[${iv}], ${itemPath})`);
349
+ lines.push(` if (_ir !== true) errors.push(..._ir.errors)`);
350
+ lines.push(` }`);
351
+ lines.push(` }`);
352
+ } else if (isSchemaObject(itemSchema)) {
353
+ const itemVar = `_item${ctx.depth}`;
354
+ const detail = generateValueChecks("", itemVar, itemPath, itemSchema, suffix, ctx, true);
355
+ if (detail.length > 0) {
356
+ lines.push(` if (Array.isArray(${raw})) {`);
357
+ lines.push(` for (let ${iv} = 0; ${iv} < ${raw}.length; ${iv}++) {`);
358
+ lines.push(` const ${itemVar} = ${raw}[${iv}]`);
359
+ lines.push(...detail.map((l) => ` ${l}`));
360
+ lines.push(` }`);
361
+ lines.push(` }`);
362
+ }
363
+ }
364
+ }
365
+ if (hasMinItems(propSchema) || hasMaxItems(propSchema) || hasUniqueItems(propSchema) && propSchema.uniqueItems === true || isSchemaObject(sp["contains"]) || Array.isArray(sp["prefixItems"]) || sp["items"] === false && !Array.isArray(sp["prefixItems"])) {
366
+ if (sp["items"] === false && !Array.isArray(sp["prefixItems"])) {
367
+ lines.push(` if (Array.isArray(${raw}) && ${raw}.length > 0) {`);
368
+ lines.push(` errors.push({ message: 'must NOT have more than 0 items', path: ${path} })`);
369
+ lines.push(` }`);
370
+ }
371
+ if (hasMinItems(propSchema)) {
372
+ lines.push(` if (Array.isArray(${raw}) && ${raw}.length < ${propSchema.minItems}) {`);
373
+ lines.push(` errors.push({ message: 'must have at least ${propSchema.minItems} items', path: ${path} })`);
374
+ lines.push(` }`);
375
+ }
376
+ if (hasMaxItems(propSchema)) {
377
+ lines.push(` if (Array.isArray(${raw}) && ${raw}.length > ${propSchema.maxItems}) {`);
378
+ lines.push(` errors.push({ message: 'must have at most ${propSchema.maxItems} items', path: ${path} })`);
379
+ lines.push(` }`);
380
+ }
381
+ if (hasUniqueItems(propSchema) && propSchema.uniqueItems === true) {
382
+ const dupCond = arrayItemsAreScalarOnly(sp) ? `new Set((${raw} as unknown[]).map((_u) => JSON.stringify(_u))).size !== ${raw}.length` : `!allUnique(${raw} as unknown[])`;
383
+ lines.push(` if (Array.isArray(${raw}) && ${dupCond}) {`);
384
+ lines.push(` errors.push({ message: 'must NOT have duplicate items', path: ${path} })`);
385
+ lines.push(` }`);
386
+ }
387
+ if (isSchemaObject(sp["contains"])) {
388
+ const min = typeof sp["minContains"] === "number" ? sp["minContains"] : 1;
389
+ const max = typeof sp["maxContains"] === "number" ? sp["maxContains"] : void 0;
390
+ const matchExpr = generateMatchesExpr("_c", sp["contains"], suffix, ctx);
391
+ const bound = max !== void 0 ? `_cn < ${min} || _cn > ${max}` : `_cn < ${min}`;
392
+ lines.push(` if (Array.isArray(${raw})) {`);
393
+ lines.push(` const _cn = (${raw} as unknown[]).filter((_c) => ${matchExpr}).length`);
394
+ lines.push(` if (${bound}) {`);
395
+ lines.push(` errors.push({ message: 'array does not contain the required matching items', path: ${path} })`);
396
+ lines.push(` }`);
397
+ lines.push(` }`);
398
+ }
399
+ const prefix = sp["prefixItems"];
400
+ if (Array.isArray(prefix)) {
401
+ lines.push(` if (Array.isArray(${raw})) {`);
402
+ for (let i = 0; i < prefix.length; i++) {
403
+ const itemChecks = generateValueChecks("", `${raw}[${i}]`, `\`${path.slice(1, -1)}/${i}\``, prefix[i], suffix, ctx);
404
+ if (itemChecks.length > 0) {
405
+ lines.push(` if (${raw}.length > ${i}) {`);
406
+ lines.push(...itemChecks.map((l) => ` ${l}`));
407
+ lines.push(` }`);
600
408
  }
409
+ }
410
+ if (sp["items"] === false || sp["additionalItems"] === false) {
411
+ lines.push(` if (${raw}.length > ${prefix.length}) {`);
412
+ lines.push(` errors.push({ message: 'must NOT have more than ${prefix.length} items', path: ${path} })`);
413
+ lines.push(` }`);
414
+ }
415
+ lines.push(` }`);
601
416
  }
602
- // Inline nested object — recurse so the nested fields are actually validated.
603
- // Unconditional: `generateInlineObjectChecks` self-gates (returns `[]` when the
604
- // schema has no object keywords) and each check is guarded by an `isObject`
605
- // runtime check, so this is a no-op for non-object schemas.
606
- lines.push(...generateInlineObjectChecks(key, propSchema, raw, suffix, ctx));
607
- return lines;
417
+ }
418
+ lines.push(...generateInlineObjectChecks(key, propSchema, raw, suffix, ctx));
419
+ return lines;
608
420
  };
609
- /**
610
- * Validates a value located at a *dynamic* key (a `patternProperties` or
611
- * `additionalProperties` value), an array item, a combinator branch, or a
612
- * `dependentSchemas` subschema against `propSchema`. `raw` and `path` are
613
- * caller-supplied expressions (e.g. `obj[_k]` and `` `${_path}/${_k}` ``) so the
614
- * checks read a runtime location. By default the leaf checks are
615
- * `!== undefined`-guarded (an absent optional value is valid); pass
616
- * `required = true` for values that must be present (array items — a sparse hole
617
- * reads as `undefined` and must fail), which drops that guard. `_key` is unused
618
- * (the location is fully encoded by `path`) but kept for positional-call parity
619
- * with the combinator generators.
620
- */
621
421
  const generateValueChecks = (_key, raw, path, propSchema, suffix, ctx, required = false) => {
622
- if (!isSchemaObject(propSchema))
623
- return [];
624
- const lines = [];
625
- // Optional values skip validation when absent, so their leaf checks are
626
- // `!== undefined`-guarded. Array items are unconditionally present — a sparse
627
- // hole reads as `undefined` and must FAIL its type/const/enum check — so
628
- // `required` drops the guard.
629
- const presence = required ? '' : `${raw} !== undefined && `;
630
- if (hasRef(propSchema)) {
631
- const vName = validatorName(refToName(propSchema.$ref, suffix));
632
- if (required) {
633
- lines.push(` const _r = ${vName}(${raw}, ${path})`);
634
- lines.push(` if (_r !== true) errors.push(..._r.errors)`);
635
- }
636
- else {
637
- lines.push(` if (${raw} !== undefined) {`);
638
- lines.push(` const _r = ${vName}(${raw}, ${path})`);
639
- lines.push(` if (_r !== true) errors.push(..._r.errors)`);
640
- lines.push(` }`);
641
- }
642
- return lines;
422
+ if (!isSchemaObject(propSchema))
423
+ return [];
424
+ const lines = [];
425
+ const presence = required ? "" : `${raw} !== undefined && `;
426
+ if (hasRef(propSchema)) {
427
+ const vName = validatorName(refToName(propSchema.$ref, suffix));
428
+ if (required) {
429
+ lines.push(` const _r = ${vName}(${raw}, ${path})`);
430
+ lines.push(` if (_r !== true) errors.push(..._r.errors)`);
431
+ } else {
432
+ lines.push(` if (${raw} !== undefined) {`);
433
+ lines.push(` const _r = ${vName}(${raw}, ${path})`);
434
+ lines.push(` if (_r !== true) errors.push(..._r.errors)`);
435
+ lines.push(` }`);
643
436
  }
644
- const instanceOf = getMjstInstanceOf(propSchema);
645
- if (instanceOf) {
646
- lines.push(` if (${presence}!(${raw} instanceof ${instanceOf})) {`);
647
- lines.push(` errors.push({ message: 'must be ${instanceOf}', path: ${path} })`);
648
- lines.push(` }`);
649
- return lines;
650
- }
651
- const primitive = getMjstPrimitive(propSchema);
652
- if (primitive) {
653
- lines.push(` if (${presence}typeof ${raw} !== "${primitive}") {`);
654
- lines.push(` errors.push({ message: 'must be ${primitive}', path: ${path} })`);
655
- lines.push(` }`);
656
- return lines;
657
- }
658
- if (hasConst(propSchema)) {
659
- const mismatch = constMismatchCondition(raw, propSchema.const);
660
- const msg = JSON.stringify(`must be ${JSON.stringify(propSchema.const)}`);
661
- lines.push(` if (${presence}${mismatch}) {`);
662
- lines.push(` errors.push({ message: ${msg}, path: ${path} })`);
663
- lines.push(` }`);
664
- return lines;
665
- }
666
- if (hasEnum(propSchema)) {
667
- const allowed = JSON.stringify(propSchema.enum);
668
- const label = propSchema.enum.map((v) => JSON.stringify(v)).join(', ');
669
- lines.push(` if (${presence}!(${allowed} as unknown[]).includes(${raw})) {`);
670
- lines.push(` errors.push({ message: ${JSON.stringify(`must be one of: ${label}`)}, path: ${path} })`);
671
- lines.push(` }`);
672
- return lines;
673
- }
674
- if (hasType(propSchema)) {
675
- const t = propSchema.type;
676
- const wrongType = wrongTypeCondition(raw, t);
677
- const typLabel = typeofString(t);
678
- if (wrongType) {
679
- lines.push(` if (${presence}(${wrongType})) {`);
680
- lines.push(` errors.push({ message: 'must be ${typLabel}', path: ${path} })`);
681
- lines.push(` }`);
682
- }
683
- }
684
- // Constraint and combinator checks run regardless of a declared `type`: they
685
- // gate on keyword presence + a runtime-type guard, so a type-less subschema
686
- // (a combinator branch like `{ required: [...] }` or `{ minItems: 2 }`) is
687
- // still validated rather than collapsing to "matches everything".
688
- //
689
- // This value lives at `path` (a template literal), so anchor the recursion's
690
- // context there and one nesting level deeper: any nested object/array it emits
691
- // then builds paths relative to THIS value and mints collision-free variable
692
- // names, independent of the caller's context. `key` is intentionally dropped
693
- // (set to `''`) because `path` already locates the value.
694
- const valueCtx = {
695
- objVar: ctx.objVar,
696
- pathPrefix: path.slice(1, -1),
697
- depth: ctx.depth + 1,
698
- hoisted: ctx.hoisted,
699
- };
700
- lines.push(...generateConstraintChecks('', raw, path, propSchema, suffix, valueCtx));
701
- lines.push(...generateCombinatorChecks('', raw, path, propSchema, suffix, valueCtx));
702
437
  return lines;
438
+ }
439
+ const instanceOf = getMjstInstanceOf(propSchema);
440
+ if (instanceOf) {
441
+ lines.push(` if (${presence}!(${raw} instanceof ${instanceOf})) {`);
442
+ lines.push(` errors.push({ message: 'must be ${instanceOf}', path: ${path} })`);
443
+ lines.push(` }`);
444
+ return lines;
445
+ }
446
+ const primitive = getMjstPrimitive(propSchema);
447
+ if (primitive) {
448
+ lines.push(` if (${presence}typeof ${raw} !== "${primitive}") {`);
449
+ lines.push(` errors.push({ message: 'must be ${primitive}', path: ${path} })`);
450
+ lines.push(` }`);
451
+ return lines;
452
+ }
453
+ if (hasConst(propSchema)) {
454
+ const mismatch = constMismatchCondition(raw, propSchema.const);
455
+ const msg = JSON.stringify(`must be ${JSON.stringify(propSchema.const)}`);
456
+ lines.push(` if (${presence}${mismatch}) {`);
457
+ lines.push(` errors.push({ message: ${msg}, path: ${path} })`);
458
+ lines.push(` }`);
459
+ return lines;
460
+ }
461
+ if (hasEnum(propSchema)) {
462
+ const allowed = JSON.stringify(propSchema.enum);
463
+ const label = propSchema.enum.map((v) => JSON.stringify(v)).join(", ");
464
+ lines.push(` if (${presence}!(${allowed} as unknown[]).includes(${raw})) {`);
465
+ lines.push(` errors.push({ message: ${JSON.stringify(`must be one of: ${label}`)}, path: ${path} })`);
466
+ lines.push(` }`);
467
+ return lines;
468
+ }
469
+ if (hasType(propSchema)) {
470
+ const t = propSchema.type;
471
+ const wrongType = wrongTypeCondition(raw, t);
472
+ const typLabel = typeofString(t);
473
+ if (wrongType) {
474
+ lines.push(` if (${presence}(${wrongType})) {`);
475
+ lines.push(` errors.push({ message: 'must be ${typLabel}', path: ${path} })`);
476
+ lines.push(` }`);
477
+ }
478
+ }
479
+ const valueCtx = {
480
+ objVar: ctx.objVar,
481
+ pathPrefix: path.slice(1, -1),
482
+ depth: ctx.depth + 1,
483
+ hoisted: ctx.hoisted
484
+ };
485
+ lines.push(...generateConstraintChecks("", raw, path, propSchema, suffix, valueCtx));
486
+ lines.push(...generateCombinatorChecks("", raw, path, propSchema, suffix, valueCtx));
487
+ return lines;
703
488
  };
704
- /**
705
- * A boolean expression that is `true` when `raw` matches `sub`. Reuses the value
706
- * checks but collects their errors into a throwaway local buffer, so the same
707
- * logic that produces error messages also answers the yes/no question the
708
- * combinators (`anyOf`/`oneOf`/`not`/`if`) and `contains` need.
709
- */
710
489
  const generateMatchesExpr = (raw, sub, suffix, ctx) => {
711
- if (sub === true)
712
- return 'true';
713
- if (sub === false)
714
- return 'false';
715
- if (!isSchemaObject(sub))
716
- return 'true';
717
- const checks = generateValueChecks('', raw, '`${_path}`', sub, suffix, ctx);
718
- if (checks.length === 0)
719
- return 'true';
720
- // The checks push to `errors`; redirect them to the IIFE-local `_m`. The outer
721
- // validator's `errors.push` `(errors ??= [])` rewrite never sees these (they
722
- // are already `_m.push`), and nested match IIFEs each shadow their own `_m`.
723
- const body = checks.join('\n').replaceAll('errors.push(', '_m.push(');
724
- return `((): boolean => { const _m: ValidationError[] = []\n${body}\n return _m.length === 0 })()`;
490
+ if (sub === true)
491
+ return "true";
492
+ if (sub === false)
493
+ return "false";
494
+ if (!isSchemaObject(sub))
495
+ return "true";
496
+ const checks = generateValueChecks("", raw, "`${_path}`", sub, suffix, ctx);
497
+ if (checks.length === 0)
498
+ return "true";
499
+ const body = checks.join("\n").replaceAll("errors.push(", "_m.push(");
500
+ return `((): boolean => { const _m: ValidationError[] = []
501
+ ${body}
502
+ return _m.length === 0 })()`;
725
503
  };
726
- /**
727
- * Emits the combinator keywords (`allOf`, `anyOf`, `oneOf`, `not`,
728
- * `if`/`then`/`else`). `allOf` surfaces each branch's errors directly; the others
729
- * evaluate branch membership as a boolean via {@link generateMatchesExpr}.
730
- */
731
504
  const generateCombinatorChecks = (key, raw, path, schema, suffix, ctx) => {
732
- if (!isSchemaObject(schema))
733
- return [];
734
- const lines = [];
735
- if (hasAllOf(schema)) {
736
- for (const branch of schema.allOf)
737
- lines.push(...generateValueChecks(key, raw, path, branch, suffix, ctx));
738
- }
739
- if (hasAnyOf(schema) && schema.anyOf.length > 0) {
740
- const conds = schema.anyOf.map((b) => generateMatchesExpr(raw, b, suffix, ctx));
741
- lines.push(` if (!(${conds.join(' || ')})) {`);
742
- lines.push(` errors.push({ message: 'must match a schema in anyOf', path: ${path} })`);
743
- lines.push(` }`);
744
- }
745
- if (hasOneOf(schema) && schema.oneOf.length > 0) {
746
- const conds = schema.oneOf.map((b) => `(${generateMatchesExpr(raw, b, suffix, ctx)} ? 1 : 0)`);
747
- lines.push(` if ((${conds.join(' + ')}) !== 1) {`);
748
- lines.push(` errors.push({ message: 'must match exactly one schema in oneOf', path: ${path} })`);
749
- lines.push(` }`);
750
- }
751
- const not = schema['not'];
752
- if (not !== undefined && (isSchemaObject(not) || typeof not === 'boolean')) {
753
- const cond = generateMatchesExpr(raw, not, suffix, ctx);
754
- lines.push(` if (${cond}) {`);
755
- lines.push(` errors.push({ message: 'must NOT match the schema in not', path: ${path} })`);
756
- lines.push(` }`);
757
- }
758
- const ifSchema = schema['if'];
759
- if (ifSchema !== undefined && (isSchemaObject(ifSchema) || typeof ifSchema === 'boolean')) {
760
- const thenSchema = schema['then'];
761
- const elseSchema = schema['else'];
762
- const thenLines = thenSchema !== undefined ? generateValueChecks(key, raw, path, thenSchema, suffix, ctx) : [];
763
- const elseLines = elseSchema !== undefined ? generateValueChecks(key, raw, path, elseSchema, suffix, ctx) : [];
764
- if (thenLines.length > 0 || elseLines.length > 0) {
765
- lines.push(` if (${generateMatchesExpr(raw, ifSchema, suffix, ctx)}) {`);
766
- lines.push(...thenLines);
767
- lines.push(` } else {`);
768
- lines.push(...elseLines);
769
- lines.push(` }`);
770
- }
771
- }
772
- return lines;
505
+ if (!isSchemaObject(schema))
506
+ return [];
507
+ const lines = [];
508
+ if (hasAllOf(schema)) {
509
+ for (const branch of schema.allOf)
510
+ lines.push(...generateValueChecks(key, raw, path, branch, suffix, ctx));
511
+ }
512
+ if (hasAnyOf(schema) && schema.anyOf.length > 0) {
513
+ const conds = schema.anyOf.map((b) => generateMatchesExpr(raw, b, suffix, ctx));
514
+ lines.push(` if (!(${conds.join(" || ")})) {`);
515
+ lines.push(` errors.push({ message: 'must match a schema in anyOf', path: ${path} })`);
516
+ lines.push(` }`);
517
+ }
518
+ if (hasOneOf(schema) && schema.oneOf.length > 0) {
519
+ const conds = schema.oneOf.map((b) => `(${generateMatchesExpr(raw, b, suffix, ctx)} ? 1 : 0)`);
520
+ lines.push(` if ((${conds.join(" + ")}) !== 1) {`);
521
+ lines.push(` errors.push({ message: 'must match exactly one schema in oneOf', path: ${path} })`);
522
+ lines.push(` }`);
523
+ }
524
+ const not = schema["not"];
525
+ if (not !== void 0 && (isSchemaObject(not) || typeof not === "boolean")) {
526
+ const cond = generateMatchesExpr(raw, not, suffix, ctx);
527
+ lines.push(` if (${cond}) {`);
528
+ lines.push(` errors.push({ message: 'must NOT match the schema in not', path: ${path} })`);
529
+ lines.push(` }`);
530
+ }
531
+ const ifSchema = schema["if"];
532
+ if (ifSchema !== void 0 && (isSchemaObject(ifSchema) || typeof ifSchema === "boolean")) {
533
+ const thenSchema = schema["then"];
534
+ const elseSchema = schema["else"];
535
+ const thenLines = thenSchema !== void 0 ? generateValueChecks(key, raw, path, thenSchema, suffix, ctx) : [];
536
+ const elseLines = elseSchema !== void 0 ? generateValueChecks(key, raw, path, elseSchema, suffix, ctx) : [];
537
+ if (thenLines.length > 0 || elseLines.length > 0) {
538
+ lines.push(` if (${generateMatchesExpr(raw, ifSchema, suffix, ctx)}) {`);
539
+ lines.push(...thenLines);
540
+ lines.push(` } else {`);
541
+ lines.push(...elseLines);
542
+ lines.push(` }`);
543
+ }
544
+ }
545
+ return lines;
773
546
  };
774
- /**
775
- * Emits validation for `patternProperties` and a schema-form
776
- * `additionalProperties` (the `false` form is handled by
777
- * {@link generateStrictKeyChecks}). For each object key, every matching
778
- * `patternProperties` subschema runs against the value; keys reached by neither
779
- * `properties` nor any pattern fall through to `additionalProperties`. This
780
- * mirrors the runtime interpreter, which validates these values rather than only
781
- * gating extra keys.
782
- */
783
547
  const generatePatternAndAdditionalChecks = (schema, suffix, ctx) => {
784
- if (!isSchemaObject(schema))
785
- return [];
786
- const obj = ctx.objVar;
787
- const d = ctx.depth;
788
- const lines = [];
789
- const patternsRecord = 'patternProperties' in schema && typeof schema.patternProperties === 'object' && schema.patternProperties !== null
790
- ? schema.patternProperties
791
- : {};
792
- const patternEntries = Object.entries(patternsRecord);
793
- for (const [pattern, sub] of patternEntries) {
794
- const re = escapeRegexPattern(pattern);
795
- const kv = `_pk${d}`;
796
- const valueChecks = generateValueChecks(`\${${kv}}`, `${obj}[${kv}]`, `\`${ctx.pathPrefix}/\${${kv}}\``, sub, suffix, ctx);
797
- if (valueChecks.length === 0)
798
- continue;
799
- lines.push(` for (const ${kv} in ${obj}) {`);
800
- lines.push(` if (/${re}/.test(${kv})) {`);
801
- lines.push(...valueChecks.map((line) => ` ${line}`));
802
- lines.push(` }`);
803
- lines.push(` }`);
804
- }
805
- // Schema-form `additionalProperties` validates every key reached by neither a
806
- // declared property nor any `patternProperties` regex.
807
- if (hasAdditionalProperties(schema) && isSchemaObject(schema.additionalProperties)) {
808
- const additional = schema.additionalProperties;
809
- const kv = `_ak${d}`;
810
- const valueChecks = generateValueChecks(`\${${kv}}`, `${obj}[${kv}]`, `\`${ctx.pathPrefix}/\${${kv}}\``, additional, suffix, ctx);
811
- if (valueChecks.length > 0) {
812
- const known = Object.keys(hasProperties(schema) ? schema.properties : {});
813
- lines.push(` for (const ${kv} in ${obj}) {`);
814
- if (known.length > 0)
815
- lines.push(` if (${JSON.stringify(known)}.includes(${kv})) continue`);
816
- for (const pattern of Object.keys(patternsRecord)) {
817
- lines.push(` if (/${escapeRegexPattern(pattern)}/.test(${kv})) continue`);
818
- }
819
- lines.push(...valueChecks.map((line) => ` ${line}`));
820
- lines.push(` }`);
821
- }
822
- }
823
- return lines;
548
+ if (!isSchemaObject(schema))
549
+ return [];
550
+ const obj = ctx.objVar;
551
+ const d = ctx.depth;
552
+ const lines = [];
553
+ const patternsRecord = "patternProperties" in schema && typeof schema.patternProperties === "object" && schema.patternProperties !== null ? schema.patternProperties : {};
554
+ const patternEntries = Object.entries(patternsRecord);
555
+ for (const [pattern, sub] of patternEntries) {
556
+ const re = escapeRegexPattern(pattern);
557
+ const kv = `_pk${d}`;
558
+ const valueChecks = generateValueChecks(`\${${kv}}`, `${obj}[${kv}]`, `\`${ctx.pathPrefix}/\${${kv}}\``, sub, suffix, ctx);
559
+ if (valueChecks.length === 0)
560
+ continue;
561
+ lines.push(` for (const ${kv} in ${obj}) {`);
562
+ lines.push(` if (/${re}/.test(${kv})) {`);
563
+ lines.push(...valueChecks.map((line) => ` ${line}`));
564
+ lines.push(` }`);
565
+ lines.push(` }`);
566
+ }
567
+ if (hasAdditionalProperties(schema) && isSchemaObject(schema.additionalProperties)) {
568
+ const additional = schema.additionalProperties;
569
+ const kv = `_ak${d}`;
570
+ const valueChecks = generateValueChecks(`\${${kv}}`, `${obj}[${kv}]`, `\`${ctx.pathPrefix}/\${${kv}}\``, additional, suffix, ctx);
571
+ if (valueChecks.length > 0) {
572
+ const known = Object.keys(hasProperties(schema) ? schema.properties : {});
573
+ lines.push(` for (const ${kv} in ${obj}) {`);
574
+ if (known.length > 0)
575
+ lines.push(` if (${JSON.stringify(known)}.includes(${kv})) continue`);
576
+ for (const pattern of Object.keys(patternsRecord)) {
577
+ lines.push(` if (/${escapeRegexPattern(pattern)}/.test(${kv})) continue`);
578
+ }
579
+ lines.push(...valueChecks.map((line) => ` ${line}`));
580
+ lines.push(` }`);
581
+ }
582
+ }
583
+ return lines;
824
584
  };
825
- /**
826
- * Generates the recursive checks for an inline nested object property, i.e. an
827
- * object schema written directly under `properties` rather than referenced via
828
- * `$ref` (those delegate to the referenced validator instead). The value is
829
- * narrowed into its own block-scoped variable and each nested property runs
830
- * through the same per-property generator, so nesting works to any depth.
831
- */
832
585
  const generateInlineObjectChecks = (key, propSchema, raw, suffix, ctx) => {
833
- if (!isSchemaObject(propSchema))
834
- return [];
835
- const child = {
836
- objVar: `_obj${ctx.depth + 1}`,
837
- // When `key` is empty the value is located AT `ctx.pathPrefix` already (e.g. an
838
- // inline object reached through a combinator branch or a dynamic-key value), so
839
- // appending `/${key}` would emit a spurious `//` or trailing `/` in error paths.
840
- pathPrefix: key === '' ? ctx.pathPrefix : `${ctx.pathPrefix}/${pointerSegment(key)}`,
841
- depth: ctx.depth + 1,
842
- hoisted: ctx.hoisted,
843
- };
844
- const required = new Set(hasRequired(propSchema) ? propSchema.required : []);
845
- const properties = hasProperties(propSchema) ? propSchema.properties : {};
846
- const innerLines = [];
847
- for (const [childKey, childSchema] of Object.entries(properties)) {
848
- innerLines.push(...generatePropertyChecks(childKey, childSchema, required.has(childKey), suffix, child));
849
- }
850
- innerLines.push(...generateMissingRequiredChecks(propSchema, child));
851
- innerLines.push(...generatePatternAndAdditionalChecks(propSchema, suffix, child));
852
- innerLines.push(...generateStrictKeyChecks(propSchema, child));
853
- innerLines.push(...generateDependentRequiredChecks(propSchema, child));
854
- innerLines.push(...generateDependentSchemasChecks(propSchema, suffix, child));
855
- innerLines.push(...generateDependenciesChecks(propSchema, suffix, child));
856
- innerLines.push(...generateMinMaxPropertiesChecks(propSchema, child));
857
- if (hasPropertyNames(propSchema) && isSchemaObject(propSchema.propertyNames)) {
858
- innerLines.push(...generatePropertyNameChecks(propSchema.propertyNames, suffix, child));
859
- }
860
- if (innerLines.length === 0)
861
- return [];
862
- // The shape check for the property itself already ran (or the property is
863
- // optional), so re-guard here instead of assuming the value is an object.
864
- return [
865
- ` if (typeof ${raw} === 'object' && ${raw} !== null && !Array.isArray(${raw})) {`,
866
- ` const ${child.objVar} = ${raw} as Record<string, unknown>`,
867
- ...innerLines.map((line) => ` ${line}`),
868
- ` }`,
869
- ];
586
+ if (!isSchemaObject(propSchema))
587
+ return [];
588
+ const child = {
589
+ objVar: `_obj${ctx.depth + 1}`,
590
+ // When `key` is empty the value is located AT `ctx.pathPrefix` already (e.g. an
591
+ // inline object reached through a combinator branch or a dynamic-key value), so
592
+ // appending `/${key}` would emit a spurious `//` or trailing `/` in error paths.
593
+ pathPrefix: key === "" ? ctx.pathPrefix : `${ctx.pathPrefix}/${pointerSegment(key)}`,
594
+ depth: ctx.depth + 1,
595
+ hoisted: ctx.hoisted
596
+ };
597
+ const required = new Set(hasRequired(propSchema) ? propSchema.required : []);
598
+ const properties = hasProperties(propSchema) ? propSchema.properties : {};
599
+ const innerLines = [];
600
+ for (const [childKey, childSchema] of Object.entries(properties)) {
601
+ innerLines.push(...generatePropertyChecks(childKey, childSchema, required.has(childKey), suffix, child));
602
+ }
603
+ innerLines.push(...generateMissingRequiredChecks(propSchema, child));
604
+ innerLines.push(...generatePatternAndAdditionalChecks(propSchema, suffix, child));
605
+ innerLines.push(...generateStrictKeyChecks(propSchema, child));
606
+ innerLines.push(...generateDependentRequiredChecks(propSchema, child));
607
+ innerLines.push(...generateDependentSchemasChecks(propSchema, suffix, child));
608
+ innerLines.push(...generateDependenciesChecks(propSchema, suffix, child));
609
+ innerLines.push(...generateMinMaxPropertiesChecks(propSchema, child));
610
+ if (hasPropertyNames(propSchema) && isSchemaObject(propSchema.propertyNames)) {
611
+ innerLines.push(...generatePropertyNameChecks(propSchema.propertyNames, suffix, child));
612
+ }
613
+ if (innerLines.length === 0)
614
+ return [];
615
+ return [
616
+ ` if (typeof ${raw} === 'object' && ${raw} !== null && !Array.isArray(${raw})) {`,
617
+ ` const ${child.objVar} = ${raw} as Record<string, unknown>`,
618
+ ...innerLines.map((line) => ` ${line}`),
619
+ ` }`
620
+ ];
870
621
  };
871
- /**
872
- * Generates the `propertyNames` loop: every object key is a string, and the
873
- * *whole* subschema is validated against each key — not just the
874
- * `pattern`/length/`enum`/`const`/`$ref` subset. Delegating to
875
- * {@link generateValueChecks} keeps the generator in lockstep with the
876
- * interpreter, which runs `matchesSchema(nameSchema, key)` per key, so a
877
- * subschema carrying a combinator, `type`, `multipleOf`, etc. is enforced too.
878
- * A key is always a present string, so the value checks run in `required` mode
879
- * (no `!== undefined` guard).
880
- */
881
622
  const generatePropertyNameChecks = (nameSchema, suffix, ctx) => {
882
- if (!isSchemaObject(nameSchema))
883
- return [];
884
- const at = `\`${ctx.pathPrefix}/\${_name}\``;
885
- const nameCtx = {
886
- objVar: ctx.objVar,
887
- pathPrefix: `${ctx.pathPrefix}/\${_name}`,
888
- depth: ctx.depth + 1,
889
- hoisted: ctx.hoisted,
890
- };
891
- const checks = generateValueChecks('', '_name', at, nameSchema, suffix, nameCtx, true);
892
- if (checks.length === 0)
893
- return [];
894
- return [` for (const _name of Object.keys(${ctx.objVar})) {`, ...checks.map((line) => ` ${line}`), ` }`];
623
+ if (!isSchemaObject(nameSchema))
624
+ return [];
625
+ const at = `\`${ctx.pathPrefix}/\${_name}\``;
626
+ const nameCtx = {
627
+ objVar: ctx.objVar,
628
+ pathPrefix: `${ctx.pathPrefix}/\${_name}`,
629
+ depth: ctx.depth + 1,
630
+ hoisted: ctx.hoisted
631
+ };
632
+ const checks = generateValueChecks("", "_name", at, nameSchema, suffix, nameCtx, true);
633
+ if (checks.length === 0)
634
+ return [];
635
+ return [` for (const _name of Object.keys(${ctx.objVar})) {`, ...checks.map((line) => ` ${line}`), ` }`];
895
636
  };
896
- /**
897
- * Emits `dependentRequired` checks: when a trigger key is present, each of its
898
- * declared dependencies must be present too. Reads the object and reports at the
899
- * current node via `ctx`, so it works at the root and inside nested objects.
900
- */
901
637
  const generateDependentRequiredChecks = (schema, ctx) => {
902
- if (!isSchemaObject(schema) || !hasDependentRequired(schema))
903
- return [];
904
- const obj = ctx.objVar;
905
- const at = ctx.depth === 0 ? '_path' : `\`${ctx.pathPrefix}\``;
906
- const lines = [];
907
- for (const [trigger, deps] of Object.entries(schema.dependentRequired)) {
908
- if (!Array.isArray(deps))
909
- continue;
910
- for (const dep of deps) {
911
- const msg = JSON.stringify(`must have property '${dep}' when '${trigger}' is present`);
912
- lines.push(` if (${JSON.stringify(trigger)} in ${obj} && !(${JSON.stringify(dep)} in ${obj})) {`);
913
- lines.push(` errors.push({ message: ${msg}, path: ${at} })`);
914
- lines.push(` }`);
915
- }
916
- }
917
- return lines;
638
+ if (!isSchemaObject(schema) || !hasDependentRequired(schema))
639
+ return [];
640
+ const obj = ctx.objVar;
641
+ const at = ctx.depth === 0 ? "_path" : `\`${ctx.pathPrefix}\``;
642
+ const lines = [];
643
+ for (const [trigger, deps] of Object.entries(schema.dependentRequired)) {
644
+ if (!Array.isArray(deps))
645
+ continue;
646
+ for (const dep of deps) {
647
+ const msg = JSON.stringify(`must have property '${dep}' when '${trigger}' is present`);
648
+ lines.push(` if (${JSON.stringify(trigger)} in ${obj} && !(${JSON.stringify(dep)} in ${obj})) {`);
649
+ lines.push(` errors.push({ message: ${msg}, path: ${at} })`);
650
+ lines.push(` }`);
651
+ }
652
+ }
653
+ return lines;
918
654
  };
919
- /**
920
- * Emits `dependentSchemas` checks (2020-12): when a trigger property is present,
921
- * the *whole object* must also match the associated subschema. Mirrors the
922
- * interpreter, which applies the subschema in place against the object. A `true`
923
- * subschema permits everything (no-op); a `false` subschema makes the trigger's
924
- * presence always invalid.
925
- */
926
655
  const generateDependentSchemasChecks = (schema, suffix, ctx) => {
927
- if (!isSchemaObject(schema))
928
- return [];
929
- const dep = schema['dependentSchemas'];
930
- if (typeof dep !== 'object' || dep === null || Array.isArray(dep))
931
- return [];
932
- const obj = ctx.objVar;
933
- const at = ctx.depth === 0 ? '_path' : `\`${ctx.pathPrefix}\``;
934
- const objPath = `\`${ctx.pathPrefix}\``;
935
- const lines = [];
936
- for (const [trigger, sub] of Object.entries(dep)) {
937
- if (sub === true)
938
- continue;
939
- if (sub === false) {
940
- const msg = JSON.stringify(`must NOT have property '${trigger}'`);
941
- lines.push(` if (${JSON.stringify(trigger)} in ${obj}) {`);
942
- lines.push(` errors.push({ message: ${msg}, path: ${at} })`);
943
- lines.push(` }`);
944
- continue;
945
- }
946
- if (!isSchemaObject(sub))
947
- continue;
948
- // The subschema applies to the object itself, so validate the current object
949
- // variable against it and gate the whole block on the trigger's presence.
950
- const checks = generateValueChecks('', obj, objPath, sub, suffix, ctx);
951
- if (checks.length === 0)
952
- continue;
953
- lines.push(` if (${JSON.stringify(trigger)} in ${obj}) {`);
954
- lines.push(...checks.map((line) => ` ${line}`));
955
- lines.push(` }`);
656
+ if (!isSchemaObject(schema))
657
+ return [];
658
+ const dep = schema["dependentSchemas"];
659
+ if (typeof dep !== "object" || dep === null || Array.isArray(dep))
660
+ return [];
661
+ const obj = ctx.objVar;
662
+ const at = ctx.depth === 0 ? "_path" : `\`${ctx.pathPrefix}\``;
663
+ const objPath = `\`${ctx.pathPrefix}\``;
664
+ const lines = [];
665
+ for (const [trigger, sub] of Object.entries(dep)) {
666
+ if (sub === true)
667
+ continue;
668
+ if (sub === false) {
669
+ const msg = JSON.stringify(`must NOT have property '${trigger}'`);
670
+ lines.push(` if (${JSON.stringify(trigger)} in ${obj}) {`);
671
+ lines.push(` errors.push({ message: ${msg}, path: ${at} })`);
672
+ lines.push(` }`);
673
+ continue;
956
674
  }
957
- return lines;
675
+ if (!isSchemaObject(sub))
676
+ continue;
677
+ const checks = generateValueChecks("", obj, objPath, sub, suffix, ctx);
678
+ if (checks.length === 0)
679
+ continue;
680
+ lines.push(` if (${JSON.stringify(trigger)} in ${obj}) {`);
681
+ lines.push(...checks.map((line) => ` ${line}`));
682
+ lines.push(` }`);
683
+ }
684
+ return lines;
958
685
  };
959
- /**
960
- * Emits draft-07 `dependencies` — the dual-form predecessor of
961
- * `dependentRequired` + `dependentSchemas`. When a trigger property is present,
962
- * an array value requires each listed key, and a schema value is applied to the
963
- * *whole object*. Mirrors the interpreter, which branches on the value's shape.
964
- * A `false` subschema makes the trigger's mere presence invalid; a `true`
965
- * subschema is a no-op.
966
- */
967
686
  const generateDependenciesChecks = (schema, suffix, ctx) => {
968
- if (!isSchemaObject(schema))
969
- return [];
970
- const dep = schema['dependencies'];
971
- if (typeof dep !== 'object' || dep === null || Array.isArray(dep))
972
- return [];
973
- const obj = ctx.objVar;
974
- const at = ctx.depth === 0 ? '_path' : `\`${ctx.pathPrefix}\``;
975
- const objPath = `\`${ctx.pathPrefix}\``;
976
- const lines = [];
977
- for (const [trigger, value] of Object.entries(dep)) {
978
- // Array form: each listed key must be present when the trigger is.
979
- if (Array.isArray(value)) {
980
- for (const key of value) {
981
- if (typeof key !== 'string')
982
- continue;
983
- const msg = JSON.stringify(`must have property '${key}' when '${trigger}' is present`);
984
- lines.push(` if (${JSON.stringify(trigger)} in ${obj} && !(${JSON.stringify(key)} in ${obj})) {`);
985
- lines.push(` errors.push({ message: ${msg}, path: ${at} })`);
986
- lines.push(` }`);
987
- }
988
- continue;
989
- }
990
- // Schema form: the subschema applies to the object itself.
991
- if (value === true)
992
- continue;
993
- if (value === false) {
994
- const msg = JSON.stringify(`must NOT have property '${trigger}'`);
995
- lines.push(` if (${JSON.stringify(trigger)} in ${obj}) {`);
996
- lines.push(` errors.push({ message: ${msg}, path: ${at} })`);
997
- lines.push(` }`);
998
- continue;
999
- }
1000
- if (!isSchemaObject(value))
1001
- continue;
1002
- const checks = generateValueChecks('', obj, objPath, value, suffix, ctx);
1003
- if (checks.length === 0)
1004
- continue;
1005
- lines.push(` if (${JSON.stringify(trigger)} in ${obj}) {`);
1006
- lines.push(...checks.map((line) => ` ${line}`));
687
+ if (!isSchemaObject(schema))
688
+ return [];
689
+ const dep = schema["dependencies"];
690
+ if (typeof dep !== "object" || dep === null || Array.isArray(dep))
691
+ return [];
692
+ const obj = ctx.objVar;
693
+ const at = ctx.depth === 0 ? "_path" : `\`${ctx.pathPrefix}\``;
694
+ const objPath = `\`${ctx.pathPrefix}\``;
695
+ const lines = [];
696
+ for (const [trigger, value] of Object.entries(dep)) {
697
+ if (Array.isArray(value)) {
698
+ for (const key of value) {
699
+ if (typeof key !== "string")
700
+ continue;
701
+ const msg = JSON.stringify(`must have property '${key}' when '${trigger}' is present`);
702
+ lines.push(` if (${JSON.stringify(trigger)} in ${obj} && !(${JSON.stringify(key)} in ${obj})) {`);
703
+ lines.push(` errors.push({ message: ${msg}, path: ${at} })`);
1007
704
  lines.push(` }`);
1008
- }
1009
- return lines;
705
+ }
706
+ continue;
707
+ }
708
+ if (value === true)
709
+ continue;
710
+ if (value === false) {
711
+ const msg = JSON.stringify(`must NOT have property '${trigger}'`);
712
+ lines.push(` if (${JSON.stringify(trigger)} in ${obj}) {`);
713
+ lines.push(` errors.push({ message: ${msg}, path: ${at} })`);
714
+ lines.push(` }`);
715
+ continue;
716
+ }
717
+ if (!isSchemaObject(value))
718
+ continue;
719
+ const checks = generateValueChecks("", obj, objPath, value, suffix, ctx);
720
+ if (checks.length === 0)
721
+ continue;
722
+ lines.push(` if (${JSON.stringify(trigger)} in ${obj}) {`);
723
+ lines.push(...checks.map((line) => ` ${line}`));
724
+ lines.push(` }`);
725
+ }
726
+ return lines;
1010
727
  };
1011
- /**
1012
- * Emits `minProperties` / `maxProperties` bounds on the object's key count,
1013
- * mirroring the interpreter (which counts the object's own enumerable keys and
1014
- * reports at the object node). Counting once into a depth-scoped local keeps the
1015
- * two bounds from re-walking the keys.
1016
- */
1017
728
  const generateMinMaxPropertiesChecks = (schema, ctx) => {
1018
- if (!isSchemaObject(schema))
1019
- return [];
1020
- const hasMin = hasMinProperties(schema);
1021
- const hasMax = hasMaxProperties(schema);
1022
- if (!hasMin && !hasMax)
1023
- return [];
1024
- const obj = ctx.objVar;
1025
- const at = ctx.depth === 0 ? '_path' : `\`${ctx.pathPrefix}\``;
1026
- const count = `_pc${ctx.depth}`;
1027
- const lines = [` const ${count} = Object.keys(${obj}).length`];
1028
- if (hasMin) {
1029
- const msg = JSON.stringify(`must have at least ${schema.minProperties} properties`);
1030
- lines.push(` if (${count} < ${schema.minProperties}) {`);
1031
- lines.push(` errors.push({ message: ${msg}, path: ${at} })`);
1032
- lines.push(` }`);
1033
- }
1034
- if (hasMax) {
1035
- const msg = JSON.stringify(`must have at most ${schema.maxProperties} properties`);
1036
- lines.push(` if (${count} > ${schema.maxProperties}) {`);
1037
- lines.push(` errors.push({ message: ${msg}, path: ${at} })`);
1038
- lines.push(` }`);
1039
- }
1040
- return lines;
729
+ if (!isSchemaObject(schema))
730
+ return [];
731
+ const hasMin = hasMinProperties(schema);
732
+ const hasMax = hasMaxProperties(schema);
733
+ if (!hasMin && !hasMax)
734
+ return [];
735
+ const obj = ctx.objVar;
736
+ const at = ctx.depth === 0 ? "_path" : `\`${ctx.pathPrefix}\``;
737
+ const count = `_pc${ctx.depth}`;
738
+ const lines = [` const ${count} = Object.keys(${obj}).length`];
739
+ if (hasMin) {
740
+ const msg = JSON.stringify(`must have at least ${schema.minProperties} properties`);
741
+ lines.push(` if (${count} < ${schema.minProperties}) {`);
742
+ lines.push(` errors.push({ message: ${msg}, path: ${at} })`);
743
+ lines.push(` }`);
744
+ }
745
+ if (hasMax) {
746
+ const msg = JSON.stringify(`must have at most ${schema.maxProperties} properties`);
747
+ lines.push(` if (${count} > ${schema.maxProperties}) {`);
748
+ lines.push(` errors.push({ message: ${msg}, path: ${at} })`);
749
+ lines.push(` }`);
750
+ }
751
+ return lines;
1041
752
  };
1042
- /**
1043
- * Builds the `&&` conditions that prove a single property is valid, or `null`
1044
- * when the property carries any keyword the slow path enforces beyond a bare
1045
- * type check (pattern, min/max, enum, const, `$ref`, items, x-mjst, …). A `null`
1046
- * makes the whole guard bail so that input still flows through the slow,
1047
- * error-collecting path — the guard only ever returns true for *provably* valid
1048
- * input, never weakening a verdict. `objAcc` is the expression yielding the
1049
- * parent object (already narrowed to a record); `key` indexes into it.
1050
- */
1051
- /**
1052
- * Whether an object schema carries a combinator keyword (`allOf`, `anyOf`,
1053
- * `oneOf`, `not`, `if`) that the error-collecting slow path enforces but neither
1054
- * flat guard can mirror. When present, both guards must bail so their early
1055
- * `return true` never accepts a document the combinator would reject.
1056
- */
1057
753
  const hasObjectLevelCombinator = (schema) => {
1058
- if (!isSchemaObject(schema))
1059
- return false;
1060
- return hasAllOf(schema) || hasAnyOf(schema) || hasOneOf(schema) || 'not' in schema || 'if' in schema;
754
+ if (!isSchemaObject(schema))
755
+ return false;
756
+ return hasAllOf(schema) || hasAnyOf(schema) || hasOneOf(schema) || "not" in schema || "if" in schema;
1061
757
  };
1062
758
  const guardPropConditions = (key, propSchema, objAcc) => {
1063
- if (!isSchemaObject(propSchema))
1064
- return null;
1065
- // Dotted access (`obj.number`) for identifier keys, bracket access otherwise.
1066
- const raw = safeAccessor(objAcc, key);
1067
- // Anything the slow path enforces past a typeof is cheaper to leave to the
1068
- // slow path than to mirror here, so bail and keep the guard sound.
1069
- if (hasRef(propSchema) ||
1070
- hasEnum(propSchema) ||
1071
- hasConst(propSchema) ||
1072
- hasOneOf(propSchema) ||
1073
- hasAnyOf(propSchema) ||
1074
- hasAllOf(propSchema) ||
1075
- 'not' in propSchema ||
1076
- 'if' in propSchema ||
1077
- getMjstInstanceOf(propSchema) !== undefined ||
1078
- getMjstPrimitive(propSchema) !== undefined ||
1079
- hasPattern(propSchema) ||
1080
- hasMinLength(propSchema) ||
1081
- hasMaxLength(propSchema) ||
1082
- hasMinimum(propSchema) ||
1083
- hasMaximum(propSchema) ||
1084
- hasExclusiveMinimum(propSchema) ||
1085
- hasExclusiveMaximum(propSchema) ||
1086
- hasMultipleOf(propSchema) ||
1087
- hasItems(propSchema)) {
1088
- return null;
1089
- }
1090
- if (!hasType(propSchema))
1091
- return null;
1092
- switch (propSchema.type) {
1093
- case 'string':
1094
- return [`typeof ${raw} === 'string'`];
1095
- case 'number':
1096
- return [`typeof ${raw} === 'number'`];
1097
- case 'integer':
1098
- return [`typeof ${raw} === 'number'`, `Number.isInteger(${raw})`];
1099
- case 'boolean':
1100
- return [`typeof ${raw} === 'boolean'`];
1101
- case 'null':
1102
- return [`${raw} === null`];
1103
- case 'object':
1104
- // Member access into the nested record is only reached after the shape
1105
- // check ahead of it in the `&&` chain, so the cast is always safe.
1106
- return guardObjectConditions(propSchema, raw, `(${raw} as Record<string, unknown>)`);
1107
- // Arrays need a per-item loop the guard can't express, and any other type
1108
- // (null, multi-type, untyped) is left to the slow path.
1109
- default:
1110
- return null;
1111
- }
759
+ if (!isSchemaObject(propSchema))
760
+ return null;
761
+ const raw = safeAccessor(objAcc, key);
762
+ if (hasRef(propSchema) || hasEnum(propSchema) || hasConst(propSchema) || hasOneOf(propSchema) || hasAnyOf(propSchema) || hasAllOf(propSchema) || "not" in propSchema || "if" in propSchema || getMjstInstanceOf(propSchema) !== void 0 || getMjstPrimitive(propSchema) !== void 0 || hasPattern(propSchema) || hasMinLength(propSchema) || hasMaxLength(propSchema) || hasMinimum(propSchema) || hasMaximum(propSchema) || hasExclusiveMinimum(propSchema) || hasExclusiveMaximum(propSchema) || hasMultipleOf(propSchema) || hasItems(propSchema)) {
763
+ return null;
764
+ }
765
+ if (!hasType(propSchema))
766
+ return null;
767
+ switch (propSchema.type) {
768
+ case "string":
769
+ return [`typeof ${raw} === 'string'`];
770
+ case "number":
771
+ return [`typeof ${raw} === 'number'`];
772
+ case "integer":
773
+ return [`typeof ${raw} === 'number'`, `Number.isInteger(${raw})`];
774
+ case "boolean":
775
+ return [`typeof ${raw} === 'boolean'`];
776
+ case "null":
777
+ return [`${raw} === null`];
778
+ case "object":
779
+ return guardObjectConditions(propSchema, raw, `(${raw} as Record<string, unknown>)`);
780
+ // Arrays need a per-item loop the guard can't express, and any other type
781
+ // (null, multi-type, untyped) is left to the slow path.
782
+ default:
783
+ return null;
784
+ }
1112
785
  };
1113
- /** A property key an array carries with a non-`undefined` value: `length`, or a
1114
- * canonical array index. A required prop on one of these can't be used to rule
1115
- * out arrays (an array's `length` is a number, an index can be anything). */
1116
786
  const ARRAY_INDEX_KEY = /^(0|[1-9]\d*)$/;
1117
- /** Schema types whose guard is a `typeof` check `typeof undefined` never passes. */
1118
- const TYPEOF_CHECKABLE_TYPES = new Set(['string', 'number', 'integer', 'boolean', 'object']);
1119
- /**
1120
- * Whether some required, typeof-guarded property proves the value can't be an
1121
- * array — letting the object shape-check drop its `!Array.isArray(...)` term. An
1122
- * array indexed by a normal key yields `undefined` (or an inherited method),
1123
- * which no `typeof === 'string' | 'number' | 'boolean' | 'object'` accepts, so
1124
- * that field check already rejects arrays. Keys an array does carry a real value
1125
- * for (`length`, numeric indices) are excluded, since those could slip through.
1126
- */
787
+ const TYPEOF_CHECKABLE_TYPES = /* @__PURE__ */ new Set(["string", "number", "integer", "boolean", "object"]);
1127
788
  const arrayRejectedByRequiredProp = (keys, required, properties) => {
1128
- for (const key of keys) {
1129
- if (!required.has(key) || key === 'length' || ARRAY_INDEX_KEY.test(key))
1130
- continue;
1131
- const propSchema = properties[key];
1132
- if (propSchema !== undefined &&
1133
- isSchemaObject(propSchema) &&
1134
- hasType(propSchema) &&
1135
- TYPEOF_CHECKABLE_TYPES.has(propSchema.type)) {
1136
- return true;
1137
- }
1138
- }
1139
- return false;
789
+ for (const key of keys) {
790
+ if (!required.has(key) || key === "length" || ARRAY_INDEX_KEY.test(key))
791
+ continue;
792
+ const propSchema = properties[key];
793
+ if (propSchema !== void 0 && isSchemaObject(propSchema) && hasType(propSchema) && TYPEOF_CHECKABLE_TYPES.has(propSchema.type)) {
794
+ return true;
795
+ }
796
+ }
797
+ return false;
1140
798
  };
1141
- /**
1142
- * Builds the allocation-free boolean guard for an object schema as a list of
1143
- * `&&` conditions, or `null` when the schema can't be proven valid by a cheap
1144
- * expression. The conditions are ordered so every member access is guarded by
1145
- * the object-shape check that precedes it in the `&&` chain.
1146
- *
1147
- * The guard only handles the happy path: every declared property must be
1148
- * required and a bare-typed scalar or a likewise-guardable nested object. Any
1149
- * optional property, object-level constraint the slow path enforces
1150
- * (`patternProperties`, `propertyNames`, `dependentRequired`, an
1151
- * `additionalProperties` *schema*), or unguardable property makes it bail, and
1152
- * the validator falls back to its full error-collecting body.
1153
- */
1154
799
  const guardObjectConditions = (schema, raw, objAcc) => {
1155
- if (!isObjectSchema(schema))
1156
- return null;
1157
- if (hasDependentRequired(schema) || hasPropertyNames(schema) || 'dependentSchemas' in schema)
1158
- return null;
1159
- if (hasMinProperties(schema) || hasMaxProperties(schema) || 'dependencies' in schema)
1160
- return null;
1161
- if (isSchemaObject(schema) && 'patternProperties' in schema)
1162
- return null;
1163
- // Object-level combinators are enforced by the slow path but cannot be mirrored
1164
- // by this flat guard, so bail — otherwise the guard's early `return true` would
1165
- // accept documents the combinators reject.
1166
- if (hasObjectLevelCombinator(schema))
1167
- return null;
1168
- let strict = false;
1169
- if (hasAdditionalProperties(schema)) {
1170
- // Only `additionalProperties: false` is guardable (via the key-count trick
1171
- // below); an additional-properties *schema* needs per-key validation.
1172
- if (schema.additionalProperties === false)
1173
- strict = true;
1174
- else
1175
- return null;
1176
- }
1177
- const required = new Set(hasRequired(schema) ? schema.required : []);
1178
- const properties = hasProperties(schema) ? schema.properties : {};
1179
- const keys = Object.keys(properties);
1180
- // A required key with no `properties` entry has no cheap guard condition, so
1181
- // defer to the slow path (which checks its presence).
1182
- for (const key of required) {
1183
- if (!Object.hasOwn(properties, key))
1184
- return null;
1185
- }
1186
- // The object shape-check only needs `!Array.isArray` when no required field
1187
- // check would already reject an array (see `arrayRejectedByRequiredProp`).
1188
- const arrayCheck = arrayRejectedByRequiredProp(keys, required, properties) ? '' : ` && !Array.isArray(${raw})`;
1189
- const conditions = [`typeof ${raw} === 'object' && ${raw} !== null${arrayCheck}`];
1190
- for (const key of keys) {
1191
- // An optional property would need an `=== undefined ||` branch and breaks
1192
- // the key-count trick, so the guard only covers all-required objects.
1193
- if (!required.has(key))
1194
- return null;
1195
- const propConditions = guardPropConditions(key, properties[key], objAcc);
1196
- if (propConditions === null)
1197
- return null;
1198
- conditions.push(...propConditions);
1199
- }
1200
- if (strict) {
1201
- // `additionalProperties: false` with every declared property required: once
1202
- // the typeof checks confirm each key is present, an exact key count proves
1203
- // there are no extras — TypeBox's trick, with no loop and no Set.
1204
- if (!keys.every((key) => required.has(key)))
1205
- return null;
1206
- conditions.push(`Object.keys(${objAcc}).length === ${keys.length}`);
1207
- }
1208
- return conditions;
800
+ if (!isObjectSchema(schema))
801
+ return null;
802
+ if (hasDependentRequired(schema) || hasPropertyNames(schema) || "dependentSchemas" in schema)
803
+ return null;
804
+ if (hasMinProperties(schema) || hasMaxProperties(schema) || "dependencies" in schema)
805
+ return null;
806
+ if (isSchemaObject(schema) && "patternProperties" in schema)
807
+ return null;
808
+ if (hasObjectLevelCombinator(schema))
809
+ return null;
810
+ let strict = false;
811
+ if (hasAdditionalProperties(schema)) {
812
+ if (schema.additionalProperties === false)
813
+ strict = true;
814
+ else
815
+ return null;
816
+ }
817
+ const required = new Set(hasRequired(schema) ? schema.required : []);
818
+ const properties = hasProperties(schema) ? schema.properties : {};
819
+ const keys = Object.keys(properties);
820
+ for (const key of required) {
821
+ if (!Object.hasOwn(properties, key))
822
+ return null;
823
+ }
824
+ const arrayCheck = arrayRejectedByRequiredProp(keys, required, properties) ? "" : ` && !Array.isArray(${raw})`;
825
+ const conditions = [`typeof ${raw} === 'object' && ${raw} !== null${arrayCheck}`];
826
+ for (const key of keys) {
827
+ if (!required.has(key))
828
+ return null;
829
+ const propConditions = guardPropConditions(key, properties[key], objAcc);
830
+ if (propConditions === null)
831
+ return null;
832
+ conditions.push(...propConditions);
833
+ }
834
+ if (strict) {
835
+ if (!keys.every((key) => required.has(key)))
836
+ return null;
837
+ conditions.push(`Object.keys(${objAcc}).length === ${keys.length}`);
838
+ }
839
+ return conditions;
1209
840
  };
1210
- /**
1211
- * Generates a validator function body for an object schema, checking each
1212
- * property's presence and type and collecting all errors.
1213
- */
1214
841
  const generateObjectValidator = (schema, typeName, suffix) => {
1215
- const vName = validatorName(typeName);
1216
- const required = new Set(hasRequired(schema) ? schema.required : []);
1217
- const properties = hasProperties(schema) ? schema.properties : {};
1218
- const ctx = createRootContext();
1219
- const propertyLines = [];
1220
- for (const [key, propSchema] of Object.entries(properties)) {
1221
- const checks = generatePropertyChecks(key, propSchema, required.has(key), suffix, ctx);
1222
- if (checks.length > 0) {
1223
- propertyLines.push(...checks);
1224
- }
1225
- }
1226
- // Required keys with no `properties` entry still need a presence check.
1227
- propertyLines.push(...generateMissingRequiredChecks(schema, ctx));
1228
- // patternProperties values and a schema-form additionalProperties are
1229
- // validated here (the `false` form is handled by generateStrictKeyChecks).
1230
- propertyLines.push(...generatePatternAndAdditionalChecks(schema, suffix, ctx));
1231
- // additionalProperties: false rejects every key not declared in properties
1232
- propertyLines.push(...generateStrictKeyChecks(schema, ctx));
1233
- // dependentRequired when a trigger property is present, its dependencies must be too.
1234
- propertyLines.push(...generateDependentRequiredChecks(schema, ctx));
1235
- // dependentSchemas — when a trigger property is present, the whole object must
1236
- // also match the associated subschema.
1237
- propertyLines.push(...generateDependentSchemasChecks(schema, suffix, ctx));
1238
- // dependencies (draft-07) the dual-form predecessor of dependentRequired +
1239
- // dependentSchemas.
1240
- propertyLines.push(...generateDependenciesChecks(schema, suffix, ctx));
1241
- // minProperties / maxProperties bound the object's key count.
1242
- propertyLines.push(...generateMinMaxPropertiesChecks(schema, ctx));
1243
- // propertyNames every key (always a string) must satisfy the subschema. This
1244
- // mirrors the interpreter, which runs the full subschema against each key.
1245
- if (hasPropertyNames(schema) && isSchemaObject(schema.propertyNames)) {
1246
- propertyLines.push(...generatePropertyNameChecks(schema.propertyNames, suffix, ctx));
1247
- }
1248
- // Combinators declared alongside the object's properties (e.g. an object with
1249
- // `allOf` refining it further) are validated against the object value itself.
1250
- propertyLines.push(...generateCombinatorChecks('', 'obj', '`${_path}`', schema, suffix, ctx));
1251
- // Lazily allocate the errors array so a valid input never builds one — the same
1252
- // allocation-free happy path the runtime interpreter uses. Each emitted
1253
- // `errors.push(...)` becomes a create-on-first-use push; nothing is allocated
1254
- // until the first actual error, so the common valid case stays alloc-free even
1255
- // when the schema is too rich for the boolean guard.
1256
- const body = (propertyLines.length > 0 ? '\n' + propertyLines.join('\n') + '\n' : '').replaceAll('errors.push(', '(errors ??= []).push(');
1257
- // Hoisted statements (e.g. known-keys Sets) come first so every call of the
1258
- // validator reuses them instead of rebuilding them.
1259
- const hoistedBlock = ctx.hoisted.length > 0 ? `${ctx.hoisted.join('\n')}\n\n` : '';
1260
- // A pure boolean guard for the happy path: when every property is present and
1261
- // well-typed (and, for strict objects, there are no extras) it returns true
1262
- // without allocating an `errors` array or touching the slow path. It returns
1263
- // true only for provably valid input; anything it can't prove cheaply falls
1264
- // through to the error-collecting path, which produces the same verdict and
1265
- // full JSON-Pointer errors. Schemas with constraints the guard can't express
1266
- // produce no guard at all (`null`), leaving behaviour unchanged.
1267
- const guard = guardObjectConditions(schema, 'input', 'obj');
1268
- // The cold, error-collecting body. When there's a guard this is a separate
1269
- // (unexported) function reached only on failure; the hot path never enters it
1270
- // unless input is actually invalid, so its size never costs the happy path.
1271
- const collectBody = (name, exported) => [
1272
- `${exported ? 'export ' : ''}const ${name} = (input: unknown, _path = ''): ValidationResult => {`,
1273
- ` const obj = input as Record<string, unknown>`,
1274
- ` if (typeof input !== 'object' || input === null || Array.isArray(input)) {`,
1275
- ` return { valid: false, errors: [{ message: 'must be object', path: _path }] }`,
1276
- ` }`,
1277
- ``,
1278
- ` let errors: ValidationError[] | undefined`,
1279
- body,
1280
- ` return errors !== undefined ? { valid: false, errors } : true`,
1281
- `}`,
1282
- ].join('\n');
1283
- // No guard: the exported validator is the error-collecting function itself.
1284
- if (!guard) {
1285
- return `${hoistedBlock}${collectBody(vName, true)}`;
1286
- }
1287
- // With a guard, keep the happy path inside the exported function — the guard
1288
- // is inlined as an early `return true`, so a valid input never pays an extra
1289
- // call — and move only the cold, error-collecting body into a separate
1290
- // (unexported) function. That keeps `validateX` itself tiny (guard + a single
1291
- // tail call) so V8 optimises it well, without the giant error body bloating
1292
- // the hot path. The exported `(input, _path?) => ValidationResult` contract
1293
- // is unchanged.
1294
- const collectName = `${vName}Errors`;
1295
- return [
1296
- `${hoistedBlock}${collectBody(collectName, false)}`,
1297
- ``,
1298
- `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1299
- ` const obj = input as Record<string, unknown>`,
1300
- ` if (`,
1301
- guard.map((condition) => ` ${condition}`).join(' &&\n'),
1302
- ` ) {`,
1303
- ` return true`,
1304
- ` }`,
1305
- ` return ${collectName}(input, _path)`,
1306
- `}`,
1307
- ].join('\n');
842
+ const vName = validatorName(typeName);
843
+ const required = new Set(hasRequired(schema) ? schema.required : []);
844
+ const properties = hasProperties(schema) ? schema.properties : {};
845
+ const ctx = createRootContext();
846
+ const propertyLines = [];
847
+ for (const [key, propSchema] of Object.entries(properties)) {
848
+ const checks = generatePropertyChecks(key, propSchema, required.has(key), suffix, ctx);
849
+ if (checks.length > 0) {
850
+ propertyLines.push(...checks);
851
+ }
852
+ }
853
+ propertyLines.push(...generateMissingRequiredChecks(schema, ctx));
854
+ propertyLines.push(...generatePatternAndAdditionalChecks(schema, suffix, ctx));
855
+ propertyLines.push(...generateStrictKeyChecks(schema, ctx));
856
+ propertyLines.push(...generateDependentRequiredChecks(schema, ctx));
857
+ propertyLines.push(...generateDependentSchemasChecks(schema, suffix, ctx));
858
+ propertyLines.push(...generateDependenciesChecks(schema, suffix, ctx));
859
+ propertyLines.push(...generateMinMaxPropertiesChecks(schema, ctx));
860
+ if (hasPropertyNames(schema) && isSchemaObject(schema.propertyNames)) {
861
+ propertyLines.push(...generatePropertyNameChecks(schema.propertyNames, suffix, ctx));
862
+ }
863
+ propertyLines.push(...generateCombinatorChecks("", "obj", "`${_path}`", schema, suffix, ctx));
864
+ const body = (propertyLines.length > 0 ? "\n" + propertyLines.join("\n") + "\n" : "").replaceAll("errors.push(", "(errors ??= []).push(");
865
+ const hoistedBlock = ctx.hoisted.length > 0 ? `${ctx.hoisted.join("\n")}
866
+
867
+ ` : "";
868
+ const guard = guardObjectConditions(schema, "input", "obj");
869
+ const collectBody = (name, exported) => [
870
+ `${exported ? "export " : ""}const ${name} = (input: unknown, _path = ''): ValidationResult => {`,
871
+ ` const obj = input as Record<string, unknown>`,
872
+ ` if (typeof input !== 'object' || input === null || Array.isArray(input)) {`,
873
+ ` return { valid: false, errors: [{ message: 'must be object', path: _path }] }`,
874
+ ` }`,
875
+ ``,
876
+ ` let errors: ValidationError[] | undefined`,
877
+ body,
878
+ ` return errors !== undefined ? { valid: false, errors } : true`,
879
+ `}`
880
+ ].join("\n");
881
+ if (!guard) {
882
+ return `${hoistedBlock}${collectBody(vName, true)}`;
883
+ }
884
+ const collectName = `${vName}Errors`;
885
+ return [
886
+ `${hoistedBlock}${collectBody(collectName, false)}`,
887
+ ``,
888
+ `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
889
+ ` const obj = input as Record<string, unknown>`,
890
+ ` if (`,
891
+ guard.map((condition) => ` ${condition}`).join(" &&\n"),
892
+ ` ) {`,
893
+ ` return true`,
894
+ ` }`,
895
+ ` return ${collectName}(input, _path)`,
896
+ `}`
897
+ ].join("\n");
1308
898
  };
1309
- /**
1310
- * Derives the boolean type-guard name from a type name.
1311
- * e.g. "InfoObject" → "isInfoObject"
1312
- */
1313
899
  const guardName = (typeName) => `is${typeName}`;
1314
- /**
1315
- * Builds the membership test for an `enum`, matching the slow path's
1316
- * `[...].includes(value)` verdict exactly. For the common all-primitive case it
1317
- * emits a parenthesized `a === x || a === y` chain — no per-call array
1318
- * allocation and no linear scan, so it stays on the allocation-free hot path —
1319
- * and falls back to `.includes` when a member is an object/array (reference
1320
- * equality) or `NaN` (where `includes`'s SameValueZero differs from `===`).
1321
- */
1322
900
  const enumMembershipExpr = (values, acc) => {
1323
- const allPrimitive = values.length > 0 &&
1324
- values.every((v) => (v === null || typeof v !== 'object') && typeof v !== 'function') &&
1325
- !values.some((v) => typeof v === 'number' && Number.isNaN(v));
1326
- if (allPrimitive) {
1327
- return `(${values.map((v) => `${acc} === ${JSON.stringify(v)}`).join(' || ')})`;
1328
- }
1329
- return `(${JSON.stringify(values)} as unknown[]).includes(${acc})`;
901
+ const allPrimitive = values.length > 0 && values.every((v) => (v === null || typeof v !== "object") && typeof v !== "function") && !values.some((v) => typeof v === "number" && Number.isNaN(v));
902
+ if (allPrimitive) {
903
+ return `(${values.map((v) => `${acc} === ${JSON.stringify(v)}`).join(" || ")})`;
904
+ }
905
+ return `(${JSON.stringify(values)} as unknown[]).includes(${acc})`;
1330
906
  };
1331
- /**
1332
- * Builds a boolean expression that is TRUE iff `acc` satisfies `schema`, with the
1333
- * *exact same verdict* as the error-collecting validator — or `null` when the
1334
- * schema carries something the flat form can't faithfully mirror ($ref, unions,
1335
- * `const`, x-mjst, etc.), in which case the whole guard falls back to calling the
1336
- * validator. Used for a property value or an array item; `acc` is the expression
1337
- * yielding the value.
1338
- */
1339
907
  const booleanLeafExpr = (schema, acc) => {
1340
- if (!isSchemaObject(schema))
1341
- return null;
1342
- // Anything whose verdict the flat form can't mirror exactly: defer to the
1343
- // validator (the caller turns a single `null` into a full fallback guard).
1344
- if (hasRef(schema) ||
1345
- hasConst(schema) ||
1346
- hasOneOf(schema) ||
1347
- 'anyOf' in schema ||
1348
- 'allOf' in schema ||
1349
- 'not' in schema ||
1350
- 'if' in schema ||
1351
- 'contains' in schema ||
1352
- 'prefixItems' in schema ||
1353
- getMjstInstanceOf(schema) !== undefined ||
1354
- getMjstPrimitive(schema) !== undefined) {
1355
- return null;
1356
- }
1357
- // enum same membership test the validator uses.
1358
- if (hasEnum(schema)) {
1359
- return enumMembershipExpr(schema.enum, acc);
1360
- }
1361
- if (!hasType(schema))
1362
- return null;
1363
- const t = schema.type;
1364
- switch (t) {
1365
- // Each constraint is the exact negation of the validator's error condition
1366
- // (`!(len < min)`, not `len >= min`) so edge values — most importantly `NaN`,
1367
- // which the validator accepts for a constrained number since `NaN < min` is
1368
- // false get the identical verdict.
1369
- case 'string': {
1370
- const parts = [`typeof ${acc} === 'string'`];
1371
- if (hasPattern(schema))
1372
- parts.push(`/${escapeRegexPattern(schema.pattern)}/.test(${acc})`);
1373
- if (hasMinLength(schema))
1374
- parts.push(`!(${acc}.length < ${schema.minLength})`);
1375
- if (hasMaxLength(schema))
1376
- parts.push(`!(${acc}.length > ${schema.maxLength})`);
1377
- return parts.join(' && ');
1378
- }
1379
- case 'number':
1380
- case 'integer': {
1381
- const parts = [`typeof ${acc} === 'number'`];
1382
- if (t === 'integer')
1383
- parts.push(`Number.isInteger(${acc})`);
1384
- if (hasMinimum(schema))
1385
- parts.push(`!(${acc} ${hasStrictExclusiveMinimum(schema) ? '<=' : '<'} ${schema.minimum})`);
1386
- if (hasMaximum(schema))
1387
- parts.push(`!(${acc} ${hasStrictExclusiveMaximum(schema) ? '>=' : '>'} ${schema.maximum})`);
1388
- if (hasExclusiveMinimum(schema))
1389
- parts.push(`!(${acc} <= ${schema.exclusiveMinimum})`);
1390
- if (hasExclusiveMaximum(schema))
1391
- parts.push(`!(${acc} >= ${schema.exclusiveMaximum})`);
1392
- if (hasMultipleOf(schema))
1393
- parts.push(multipleOfPassExpr(acc, schema.multipleOf));
1394
- return parts.join(' && ');
1395
- }
1396
- case 'boolean':
1397
- return `typeof ${acc} === 'boolean'`;
1398
- case 'null':
1399
- return `${acc} === null`;
1400
- case 'object': {
1401
- const parts = booleanObjectParts(schema, acc, `(${acc} as Record<string, unknown>)`);
1402
- return parts === null ? null : parts.join(' && ');
1403
- }
1404
- case 'array':
1405
- return booleanArrayExpr(schema, acc);
1406
- default:
1407
- return null;
1408
- }
908
+ if (!isSchemaObject(schema))
909
+ return null;
910
+ if (hasRef(schema) || hasConst(schema) || hasOneOf(schema) || "anyOf" in schema || "allOf" in schema || "not" in schema || "if" in schema || "contains" in schema || "prefixItems" in schema || getMjstInstanceOf(schema) !== void 0 || getMjstPrimitive(schema) !== void 0) {
911
+ return null;
912
+ }
913
+ if (hasEnum(schema)) {
914
+ return enumMembershipExpr(schema.enum, acc);
915
+ }
916
+ if (!hasType(schema))
917
+ return null;
918
+ const t = schema.type;
919
+ switch (t) {
920
+ // Each constraint is the exact negation of the validator's error condition
921
+ // (`!(len < min)`, not `len >= min`) so edge values — most importantly `NaN`,
922
+ // which the validator accepts for a constrained number since `NaN < min` is
923
+ // false — get the identical verdict.
924
+ case "string": {
925
+ const parts = [`typeof ${acc} === 'string'`];
926
+ if (hasPattern(schema))
927
+ parts.push(`/${escapeRegexPattern(schema.pattern)}/.test(${acc})`);
928
+ if (hasMinLength(schema))
929
+ parts.push(`!(${acc}.length < ${schema.minLength})`);
930
+ if (hasMaxLength(schema))
931
+ parts.push(`!(${acc}.length > ${schema.maxLength})`);
932
+ return parts.join(" && ");
933
+ }
934
+ case "number":
935
+ case "integer": {
936
+ const parts = [`typeof ${acc} === 'number'`];
937
+ if (t === "integer")
938
+ parts.push(`Number.isInteger(${acc})`);
939
+ if (hasMinimum(schema))
940
+ parts.push(`!(${acc} ${hasStrictExclusiveMinimum(schema) ? "<=" : "<"} ${schema.minimum})`);
941
+ if (hasMaximum(schema))
942
+ parts.push(`!(${acc} ${hasStrictExclusiveMaximum(schema) ? ">=" : ">"} ${schema.maximum})`);
943
+ if (hasExclusiveMinimum(schema))
944
+ parts.push(`!(${acc} <= ${schema.exclusiveMinimum})`);
945
+ if (hasExclusiveMaximum(schema))
946
+ parts.push(`!(${acc} >= ${schema.exclusiveMaximum})`);
947
+ if (hasMultipleOf(schema))
948
+ parts.push(multipleOfPassExpr(acc, schema.multipleOf));
949
+ return parts.join(" && ");
950
+ }
951
+ case "boolean":
952
+ return `typeof ${acc} === 'boolean'`;
953
+ case "null":
954
+ return `${acc} === null`;
955
+ case "object": {
956
+ const parts = booleanObjectParts(schema, acc, `(${acc} as Record<string, unknown>)`);
957
+ return parts === null ? null : parts.join(" && ");
958
+ }
959
+ case "array":
960
+ return booleanArrayExpr(schema, acc);
961
+ default:
962
+ return null;
963
+ }
1409
964
  };
1410
- /**
1411
- * Boolean expression for an array value. Mirrors the validator: array shape,
1412
- * `minItems`/`maxItems`/`uniqueItems`, and each item validated in full via
1413
- * {@link booleanLeafExpr}. Returns `null` for `$ref` items, or when an item schema
1414
- * can't be expressed flat, so the whole guard defers to the validator.
1415
- *
1416
- * Item iteration goes through `Array.from` rather than `Array.prototype.every`
1417
- * because `every` *skips holes* in a sparse array (`[, 'x']`), whereas the
1418
- * validator's index-based `for` loop reads a hole as `undefined` and rejects it.
1419
- * Materialising the array first makes the guard's verdict match the slow path's
1420
- * on sparse input — the guard must never accept what the slow path would reject.
1421
- */
1422
965
  const booleanArrayExpr = (schema, acc) => {
1423
- const parts = [`Array.isArray(${acc})`];
1424
- // Length / uniqueness, mirroring the validator's checks exactly so the guard's
1425
- // verdict matches the slow path's.
1426
- if (hasMinItems(schema))
1427
- parts.push(`${acc}.length >= ${schema.minItems}`);
1428
- if (hasMaxItems(schema))
1429
- parts.push(`${acc}.length <= ${schema.maxItems}`);
1430
- if (hasUniqueItems(schema) && schema.uniqueItems === true) {
1431
- // Same scalar-vs-structural split as the validator, so the guard's verdict
1432
- // matches the slow path's for object items in a reordered key order.
1433
- parts.push(arrayItemsAreScalarOnly(schema)
1434
- ? `new Set((${acc} as unknown[]).map((_u) => JSON.stringify(_u))).size === ${acc}.length`
1435
- : `allUnique(${acc} as unknown[])`);
1436
- }
1437
- const base = parts.join(' && ');
1438
- if (!hasItems(schema))
1439
- return base;
1440
- const items = schema.items;
1441
- if (!isSchemaObject(items))
1442
- return base;
1443
- if (hasRef(items))
1444
- return null;
1445
- // Validate each item in full, mirroring the validator's per-item checks so the
1446
- // guard reaches the identical verdict. `booleanLeafExpr` returns `null` for item
1447
- // schemas it can't express flat — bail so the validator decides, keeping the
1448
- // guard from ever accepting what the slow path would reject.
1449
- const itemExpr = booleanLeafExpr(items, '_it');
1450
- if (itemExpr === null)
1451
- return null;
1452
- return `${base} && Array.from(${acc} as unknown[]).every((_it) => (${itemExpr}))`;
966
+ const parts = [`Array.isArray(${acc})`];
967
+ if (hasMinItems(schema))
968
+ parts.push(`${acc}.length >= ${schema.minItems}`);
969
+ if (hasMaxItems(schema))
970
+ parts.push(`${acc}.length <= ${schema.maxItems}`);
971
+ if (hasUniqueItems(schema) && schema.uniqueItems === true) {
972
+ parts.push(arrayItemsAreScalarOnly(schema) ? `new Set((${acc} as unknown[]).map((_u) => JSON.stringify(_u))).size === ${acc}.length` : `allUnique(${acc} as unknown[])`);
973
+ }
974
+ const base = parts.join(" && ");
975
+ if (!hasItems(schema))
976
+ return base;
977
+ const items = schema.items;
978
+ if (!isSchemaObject(items))
979
+ return base;
980
+ if (hasRef(items))
981
+ return null;
982
+ const itemExpr = booleanLeafExpr(items, "_it");
983
+ if (itemExpr === null)
984
+ return null;
985
+ return `${base} && Array.from(${acc} as unknown[]).every((_it) => (${itemExpr}))`;
1453
986
  };
1454
- /**
1455
- * Builds the `&&` conditions proving an object value is valid (same verdict as
1456
- * the error-collecting validator), or `null` when any property or object-level
1457
- * keyword can't be mirrored flat. `raw` yields the value (for the shape check);
1458
- * `objAcc` is the same value narrowed to a record (for member access).
1459
- */
1460
987
  const booleanObjectParts = (schema, raw, objAcc) => {
1461
- if (!isObjectSchema(schema))
1462
- return null;
1463
- // These need per-key loops or cross-references the flat form can't express.
1464
- if (hasDependentRequired(schema) || hasPropertyNames(schema) || 'dependentSchemas' in schema)
1465
- return null;
1466
- if (hasMinProperties(schema) || hasMaxProperties(schema) || 'dependencies' in schema)
1467
- return null;
1468
- if (isSchemaObject(schema) && 'patternProperties' in schema)
1469
- return null;
1470
- // Object-level combinators change the verdict but can't be expressed flat, so
1471
- // defer to the validator rather than emit a guard that ignores them.
1472
- if (hasObjectLevelCombinator(schema))
1473
- return null;
1474
- let strict = false;
1475
- if (hasAdditionalProperties(schema)) {
1476
- // Only `additionalProperties: false` is expressible; a schema needs per-key
1477
- // validation, so defer to the validator.
1478
- if (schema.additionalProperties === false)
1479
- strict = true;
1480
- else
1481
- return null;
1482
- }
1483
- const required = new Set(hasRequired(schema) ? schema.required : []);
1484
- const properties = hasProperties(schema) ? schema.properties : {};
1485
- const keys = Object.keys(properties);
1486
- // Drop the `!Array.isArray` term when a required, typeof-guarded property
1487
- // already rejects arrays (an array's normal key is `undefined`, which no
1488
- // `typeof` accepts) the same sound optimisation the validator's hot guard
1489
- // uses. Kept when no such property exists.
1490
- const arrayCheck = arrayRejectedByRequiredProp(keys, required, properties) ? '' : ` && !Array.isArray(${raw})`;
1491
- const parts = [`typeof ${raw} === 'object' && ${raw} !== null${arrayCheck}`];
1492
- for (const key of keys) {
1493
- const propSchema = properties[key];
1494
- if (propSchema === undefined || !isSchemaObject(propSchema))
1495
- return null;
1496
- const member = safeAccessor(objAcc, key);
1497
- const expr = booleanLeafExpr(propSchema, member);
1498
- if (expr === null)
1499
- return null;
1500
- parts.push(required.has(key) ? expr : `(${member} === undefined || (${expr}))`);
1501
- }
1502
- if (strict) {
1503
- // `additionalProperties: false`: with every property required, an exact key
1504
- // count proves no extras (the typeof checks above already proved presence);
1505
- // otherwise sweep the keys against the declared set.
1506
- if (keys.length === 0) {
1507
- parts.push(`Object.keys(${objAcc}).length === 0`);
1508
- }
1509
- else if (keys.every((key) => required.has(key))) {
1510
- parts.push(`Object.keys(${objAcc}).length === ${keys.length}`);
1511
- }
1512
- else {
1513
- const known = keys.map((key) => `_k === ${JSON.stringify(key)}`).join(' || ');
1514
- parts.push(`Object.keys(${objAcc}).every((_k) => ${known})`);
1515
- }
1516
- }
1517
- return parts;
1518
- };
1519
- /**
1520
- * Generates the exported boolean type-guard `isTypeName(input): input is TypeName`.
1521
- *
1522
- * Unlike `validateTypeName` (which returns rich `ValidationResult` errors), this
1523
- * is a single flat boolean predicate — no error array, no cold-path call — so V8
1524
- * inlines it like a hand-written `check`, matching the shape of TypeBox's
1525
- * compiled checker. It returns the *same verdict* as the validator. When the
1526
- * schema carries anything the flat form can't mirror exactly, it falls back to
1527
- * `validateTypeName(input) === true`, which is always correct.
1528
- */
1529
- export const generateBooleanGuard = (schema, typeName, _suffix = '') => {
1530
- const name = guardName(typeName);
1531
- const fallback = `export const ${name} = (input: unknown): input is ${typeName} => ${validatorName(typeName)}(input) === true`;
1532
- // Fold `nullable: true` into `anyOf` so the guard's verdict matches the
1533
- // validator's (which applies the same rewrite). Without this a nullable node's
1534
- // guard would reject `null` while the validator accepts it.
1535
- const rewritten = rewriteNullable(schema);
1536
- if (isObjectSchema(rewritten)) {
1537
- const parts = booleanObjectParts(rewritten, 'input', 'obj');
1538
- if (parts === null)
1539
- return fallback;
1540
- return [
1541
- `export const ${name} = (input: unknown): input is ${typeName} => {`,
1542
- ` const obj = input as Record<string, unknown>`,
1543
- ` return (`,
1544
- parts.map((part) => ` ${part}`).join(' &&\n'),
1545
- ` )`,
1546
- `}`,
1547
- ].join('\n');
1548
- }
1549
- // Non-object roots (scalar, enum, array) can often be expressed inline too.
1550
- const expr = booleanLeafExpr(rewritten, 'input');
988
+ if (!isObjectSchema(schema))
989
+ return null;
990
+ if (hasDependentRequired(schema) || hasPropertyNames(schema) || "dependentSchemas" in schema)
991
+ return null;
992
+ if (hasMinProperties(schema) || hasMaxProperties(schema) || "dependencies" in schema)
993
+ return null;
994
+ if (isSchemaObject(schema) && "patternProperties" in schema)
995
+ return null;
996
+ if (hasObjectLevelCombinator(schema))
997
+ return null;
998
+ let strict = false;
999
+ if (hasAdditionalProperties(schema)) {
1000
+ if (schema.additionalProperties === false)
1001
+ strict = true;
1002
+ else
1003
+ return null;
1004
+ }
1005
+ const required = new Set(hasRequired(schema) ? schema.required : []);
1006
+ const properties = hasProperties(schema) ? schema.properties : {};
1007
+ const keys = Object.keys(properties);
1008
+ const arrayCheck = arrayRejectedByRequiredProp(keys, required, properties) ? "" : ` && !Array.isArray(${raw})`;
1009
+ const parts = [`typeof ${raw} === 'object' && ${raw} !== null${arrayCheck}`];
1010
+ for (const key of keys) {
1011
+ const propSchema = properties[key];
1012
+ if (propSchema === void 0 || !isSchemaObject(propSchema))
1013
+ return null;
1014
+ const member = safeAccessor(objAcc, key);
1015
+ const expr = booleanLeafExpr(propSchema, member);
1551
1016
  if (expr === null)
1552
- return fallback;
1553
- return `export const ${name} = (input: unknown): input is ${typeName} => ${expr}`;
1017
+ return null;
1018
+ parts.push(required.has(key) ? expr : `(${member} === undefined || (${expr}))`);
1019
+ }
1020
+ if (strict) {
1021
+ if (keys.length === 0) {
1022
+ parts.push(`Object.keys(${objAcc}).length === 0`);
1023
+ } else if (keys.every((key) => required.has(key))) {
1024
+ parts.push(`Object.keys(${objAcc}).length === ${keys.length}`);
1025
+ } else {
1026
+ const known = keys.map((key) => `_k === ${JSON.stringify(key)}`).join(" || ");
1027
+ parts.push(`Object.keys(${objAcc}).every((_k) => ${known})`);
1028
+ }
1029
+ }
1030
+ return parts;
1031
+ };
1032
+ const generateBooleanGuard = (schema, typeName, _suffix = "") => {
1033
+ const name = guardName(typeName);
1034
+ const fallback = `export const ${name} = (input: unknown): input is ${typeName} => ${validatorName(typeName)}(input) === true`;
1035
+ const rewritten = rewriteNullable(schema);
1036
+ if (isObjectSchema(rewritten)) {
1037
+ const parts = booleanObjectParts(rewritten, "input", "obj");
1038
+ if (parts === null)
1039
+ return fallback;
1040
+ return [
1041
+ `export const ${name} = (input: unknown): input is ${typeName} => {`,
1042
+ ` const obj = input as Record<string, unknown>`,
1043
+ ` return (`,
1044
+ parts.map((part) => ` ${part}`).join(" &&\n"),
1045
+ ` )`,
1046
+ `}`
1047
+ ].join("\n");
1048
+ }
1049
+ const expr = booleanLeafExpr(rewritten, "input");
1050
+ if (expr === null)
1051
+ return fallback;
1052
+ return `export const ${name} = (input: unknown): input is ${typeName} => ${expr}`;
1554
1053
  };
1555
- /**
1556
- * Generates a validator function for a non-object schema (primitive, array, enum, $ref).
1557
- */
1558
1054
  const generateScalarValidator = (schema, typeName, suffix) => {
1559
- const vName = validatorName(typeName);
1560
- if (!isSchemaObject(schema)) {
1561
- return [`export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`, ` return true`, `}`].join('\n');
1562
- }
1563
- // Top-level $ref — delegate entirely
1564
- if (hasRef(schema)) {
1565
- const delegateName = validatorName(refToName(schema.$ref, suffix));
1566
- return [
1567
- `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1568
- ` return ${delegateName}(input, _path)`,
1569
- `}`,
1570
- ].join('\n');
1571
- }
1572
- // Top-level x-mjst instanceOf (e.g. a schema that is itself a Date)
1573
- const instanceOf = getMjstInstanceOf(schema);
1574
- if (instanceOf) {
1575
- return [
1576
- `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1577
- ` if (!(input instanceof ${instanceOf})) {`,
1578
- ` return { valid: false, errors: [{ message: 'must be ${instanceOf}', path: _path }] }`,
1579
- ` }`,
1580
- ` return true`,
1581
- `}`,
1582
- ].join('\n');
1583
- }
1584
- // Top-level x-mjst primitive (e.g. a schema that is itself a bigint)
1585
- const primitive = getMjstPrimitive(schema);
1586
- if (primitive) {
1587
- return [
1588
- `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1589
- ` if (typeof input !== "${primitive}") {`,
1590
- ` return { valid: false, errors: [{ message: 'must be ${primitive}', path: _path }] }`,
1591
- ` }`,
1592
- ` return true`,
1593
- `}`,
1594
- ].join('\n');
1595
- }
1596
- // Top-level const
1597
- if (hasConst(schema)) {
1598
- const mismatch = constMismatchCondition('input', schema.const);
1599
- const msg = JSON.stringify(`must be ${JSON.stringify(schema.const)}`);
1600
- return [
1601
- `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1602
- ` if (${mismatch}) {`,
1603
- ` return { valid: false, errors: [{ message: ${msg}, path: _path }] }`,
1604
- ` }`,
1605
- ` return true`,
1606
- `}`,
1607
- ].join('\n');
1608
- }
1609
- // Top-level enum
1610
- if (hasEnum(schema)) {
1611
- const allowed = JSON.stringify(schema.enum);
1612
- const label = schema.enum.map((v) => JSON.stringify(v)).join(', ');
1613
- return [
1614
- `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1615
- ` if (!(${allowed} as unknown[]).includes(input)) {`,
1616
- ` return { valid: false, errors: [{ message: ${JSON.stringify(`must be one of: ${label}`)}, path: _path }] }`,
1617
- ` }`,
1618
- ` return true`,
1619
- `}`,
1620
- ].join('\n');
1621
- }
1622
- // oneOf — try each branch, return errors from all if none match
1623
- // Top-level combinators (`allOf` / `anyOf` / `oneOf` / `not` / `if`), validated
1624
- // against the input via the shared combinator generator — correct `oneOf`
1625
- // (exactly one) and inline branches included, not just `$ref` branches.
1626
- if (hasAllOf(schema) || hasAnyOf(schema) || hasOneOf(schema) || 'not' in schema || 'if' in schema) {
1627
- const ctx = createRootContext();
1628
- const checks = [];
1629
- // The root path expression the shared emitters use, as a template literal body.
1630
- const rootPath = '`${_path}`';
1631
- // A `type` (and its sibling value constraints) alongside a combinator still
1632
- // applies the value must satisfy BOTH. Emit the type/constraint checks first,
1633
- // then the combinator checks, so a schema like `{ type: 'string', not: {…} }`
1634
- // or `{ type: 'number', minimum: 10, allOf: [{ maximum: 100 }] }` no longer
1635
- // drops the `type` check and its siblings.
1636
- const rootTypeArray = getTypeArray(schema);
1637
- if (rootTypeArray) {
1638
- const allWrong = rootTypeArray
1639
- .map((t) => wrongTypeCondition('input', t))
1640
- .filter((c) => c !== '')
1641
- .map((c) => `(${c})`)
1642
- .join(' && ');
1643
- if (allWrong) {
1644
- const label = rootTypeArray.map((t) => typeofString(t)).join(' or ');
1645
- checks.push(` if (${allWrong}) {`);
1646
- checks.push(` errors.push({ message: ${JSON.stringify(`must be ${label}`)}, path: ${rootPath} })`);
1647
- checks.push(` }`);
1648
- }
1649
- checks.push(...generateConstraintChecks('', 'input', rootPath, schema, suffix, ctx));
1650
- }
1651
- else if (hasType(schema)) {
1652
- const t = schema.type;
1653
- const wrongType = wrongTypeCondition('input', t);
1654
- if (wrongType) {
1655
- checks.push(` if (${wrongType}) {`);
1656
- checks.push(` errors.push({ message: 'must be ${typeofString(t)}', path: ${rootPath} })`);
1657
- checks.push(` }`);
1658
- }
1659
- checks.push(...generateConstraintChecks('', 'input', rootPath, schema, suffix, ctx));
1660
- }
1661
- checks.push(...generateCombinatorChecks('', 'input', rootPath, schema, suffix, ctx));
1662
- const body = checks.join('\n').replaceAll('errors.push(', '(errors ??= []).push(');
1663
- const hoistedBlock = ctx.hoisted.length > 0 ? `${ctx.hoisted.join('\n')}\n\n` : '';
1664
- return [
1665
- `${hoistedBlock}export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1666
- ` let errors: ValidationError[] | undefined`,
1667
- body,
1668
- ` return errors !== undefined ? { valid: false, errors } : true`,
1669
- `}`,
1670
- ].join('\n');
1671
- }
1672
- // Top-level multi-type / nullable schema (array `type`, e.g. `["string","null"]`).
1673
- // `hasType` is false for an array `type`, so without this a root multi-type
1674
- // schema falls through to the final `return true` and validates NOTHING. The
1675
- // value is valid when it matches any listed type.
1676
- const rootTypeArray = getTypeArray(schema);
1677
- if (rootTypeArray) {
1678
- const allWrong = rootTypeArray
1679
- .map((t) => wrongTypeCondition('input', t))
1680
- .filter((c) => c !== '')
1681
- .map((c) => `(${c})`)
1682
- .join(' && ');
1683
- const label = rootTypeArray.map((t) => typeofString(t)).join(' or ');
1684
- if (!allWrong) {
1685
- return [
1686
- `export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`,
1687
- ` return true`,
1688
- `}`,
1689
- ].join('\n');
1690
- }
1691
- return [
1692
- `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1693
- ` if (${allWrong}) {`,
1694
- ` return { valid: false, errors: [{ message: ${JSON.stringify(`must be ${label}`)}, path: _path }] }`,
1695
- ` }`,
1696
- ` return true`,
1697
- `}`,
1698
- ].join('\n');
1055
+ const vName = validatorName(typeName);
1056
+ if (!isSchemaObject(schema)) {
1057
+ return [`export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`, ` return true`, `}`].join("\n");
1058
+ }
1059
+ if (hasRef(schema)) {
1060
+ const delegateName = validatorName(refToName(schema.$ref, suffix));
1061
+ return [
1062
+ `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1063
+ ` return ${delegateName}(input, _path)`,
1064
+ `}`
1065
+ ].join("\n");
1066
+ }
1067
+ const instanceOf = getMjstInstanceOf(schema);
1068
+ if (instanceOf) {
1069
+ return [
1070
+ `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1071
+ ` if (!(input instanceof ${instanceOf})) {`,
1072
+ ` return { valid: false, errors: [{ message: 'must be ${instanceOf}', path: _path }] }`,
1073
+ ` }`,
1074
+ ` return true`,
1075
+ `}`
1076
+ ].join("\n");
1077
+ }
1078
+ const primitive = getMjstPrimitive(schema);
1079
+ if (primitive) {
1080
+ return [
1081
+ `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1082
+ ` if (typeof input !== "${primitive}") {`,
1083
+ ` return { valid: false, errors: [{ message: 'must be ${primitive}', path: _path }] }`,
1084
+ ` }`,
1085
+ ` return true`,
1086
+ `}`
1087
+ ].join("\n");
1088
+ }
1089
+ if (hasConst(schema)) {
1090
+ const mismatch = constMismatchCondition("input", schema.const);
1091
+ const msg = JSON.stringify(`must be ${JSON.stringify(schema.const)}`);
1092
+ return [
1093
+ `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1094
+ ` if (${mismatch}) {`,
1095
+ ` return { valid: false, errors: [{ message: ${msg}, path: _path }] }`,
1096
+ ` }`,
1097
+ ` return true`,
1098
+ `}`
1099
+ ].join("\n");
1100
+ }
1101
+ if (hasEnum(schema)) {
1102
+ const allowed = JSON.stringify(schema.enum);
1103
+ const label = schema.enum.map((v) => JSON.stringify(v)).join(", ");
1104
+ return [
1105
+ `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1106
+ ` if (!(${allowed} as unknown[]).includes(input)) {`,
1107
+ ` return { valid: false, errors: [{ message: ${JSON.stringify(`must be one of: ${label}`)}, path: _path }] }`,
1108
+ ` }`,
1109
+ ` return true`,
1110
+ `}`
1111
+ ].join("\n");
1112
+ }
1113
+ if (hasAllOf(schema) || hasAnyOf(schema) || hasOneOf(schema) || "not" in schema || "if" in schema) {
1114
+ const ctx = createRootContext();
1115
+ const checks = [];
1116
+ const rootPath = "`${_path}`";
1117
+ const rootTypeArray2 = getTypeArray(schema);
1118
+ if (rootTypeArray2) {
1119
+ const allWrong = rootTypeArray2.map((t) => wrongTypeCondition("input", t)).filter((c) => c !== "").map((c) => `(${c})`).join(" && ");
1120
+ if (allWrong) {
1121
+ const label = rootTypeArray2.map((t) => typeofString(t)).join(" or ");
1122
+ checks.push(` if (${allWrong}) {`);
1123
+ checks.push(` errors.push({ message: ${JSON.stringify(`must be ${label}`)}, path: ${rootPath} })`);
1124
+ checks.push(` }`);
1125
+ }
1126
+ checks.push(...generateConstraintChecks("", "input", rootPath, schema, suffix, ctx));
1127
+ } else if (hasType(schema)) {
1128
+ const t = schema.type;
1129
+ const wrongType = wrongTypeCondition("input", t);
1130
+ if (wrongType) {
1131
+ checks.push(` if (${wrongType}) {`);
1132
+ checks.push(` errors.push({ message: 'must be ${typeofString(t)}', path: ${rootPath} })`);
1133
+ checks.push(` }`);
1134
+ }
1135
+ checks.push(...generateConstraintChecks("", "input", rootPath, schema, suffix, ctx));
1136
+ }
1137
+ checks.push(...generateCombinatorChecks("", "input", rootPath, schema, suffix, ctx));
1138
+ const body = checks.join("\n").replaceAll("errors.push(", "(errors ??= []).push(");
1139
+ const hoistedBlock = ctx.hoisted.length > 0 ? `${ctx.hoisted.join("\n")}
1140
+
1141
+ ` : "";
1142
+ return [
1143
+ `${hoistedBlock}export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1144
+ ` let errors: ValidationError[] | undefined`,
1145
+ body,
1146
+ ` return errors !== undefined ? { valid: false, errors } : true`,
1147
+ `}`
1148
+ ].join("\n");
1149
+ }
1150
+ const rootTypeArray = getTypeArray(schema);
1151
+ if (rootTypeArray) {
1152
+ const allWrong = rootTypeArray.map((t) => wrongTypeCondition("input", t)).filter((c) => c !== "").map((c) => `(${c})`).join(" && ");
1153
+ const label = rootTypeArray.map((t) => typeofString(t)).join(" or ");
1154
+ if (!allWrong) {
1155
+ return [
1156
+ `export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`,
1157
+ ` return true`,
1158
+ `}`
1159
+ ].join("\n");
1699
1160
  }
1700
- // Top-level typed schema (string, number, boolean, array)
1701
- if (hasType(schema)) {
1702
- const t = schema.type;
1703
- const wrongType = wrongTypeCondition('input', t);
1704
- const typLabel = typeofString(t);
1705
- // Reuse the shared constraint emitter — the per-property path already handles
1706
- // string (pattern, min/maxLength), number/integer (bounds, multipleOf) and
1707
- // array (items, min/maxItems, uniqueItems, contains, prefixItems). The root
1708
- // path previously only built string constraints, so a `{type:'number',
1709
- // minimum:5}` or `{type:'array', minItems:2}` root accepted invalid input.
1710
- // `raw` is `input`; `path` is the root `_path` (as a template so the shared
1711
- // emitter's `path.slice(1,-1)` for array-item indices still works).
1712
- const rootCtx = createRootContext();
1713
- const constraintLines = generateConstraintChecks('', 'input', '`${_path}`', schema, suffix, rootCtx);
1714
- if (!wrongType) {
1715
- return [
1716
- `export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`,
1717
- ` return true`,
1718
- `}`,
1719
- ].join('\n');
1720
- }
1721
- if (constraintLines.length === 0) {
1722
- return [
1723
- `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1724
- ` if (${wrongType}) {`,
1725
- ` return { valid: false, errors: [{ message: 'must be ${typLabel}', path: _path }] }`,
1726
- ` }`,
1727
- ` return true`,
1728
- `}`,
1729
- ].join('\n');
1730
- }
1731
- // Array-item / nested constraints can hoist module-level declarations (e.g. a
1732
- // compiled known-keys set); emit them before the function so it references them.
1733
- const hoistedBlock = rootCtx.hoisted.length > 0 ? `${rootCtx.hoisted.join('\n')}\n\n` : '';
1734
- return [
1735
- `${hoistedBlock}export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1736
- ` if (${wrongType}) {`,
1737
- ` return { valid: false, errors: [{ message: 'must be ${typLabel}', path: _path }] }`,
1738
- ` }`,
1739
- ` let errors: ValidationError[] | undefined`,
1740
- constraintLines.join('\n').replaceAll('errors.push(', '(errors ??= []).push('),
1741
- ` return errors !== undefined ? { valid: false, errors } : true`,
1742
- `}`,
1743
- ].join('\n');
1161
+ return [
1162
+ `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1163
+ ` if (${allWrong}) {`,
1164
+ ` return { valid: false, errors: [{ message: ${JSON.stringify(`must be ${label}`)}, path: _path }] }`,
1165
+ ` }`,
1166
+ ` return true`,
1167
+ `}`
1168
+ ].join("\n");
1169
+ }
1170
+ if (hasType(schema)) {
1171
+ const t = schema.type;
1172
+ const wrongType = wrongTypeCondition("input", t);
1173
+ const typLabel = typeofString(t);
1174
+ const rootCtx = createRootContext();
1175
+ const constraintLines = generateConstraintChecks("", "input", "`${_path}`", schema, suffix, rootCtx);
1176
+ if (!wrongType) {
1177
+ return [
1178
+ `export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`,
1179
+ ` return true`,
1180
+ `}`
1181
+ ].join("\n");
1182
+ }
1183
+ if (constraintLines.length === 0) {
1184
+ return [
1185
+ `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1186
+ ` if (${wrongType}) {`,
1187
+ ` return { valid: false, errors: [{ message: 'must be ${typLabel}', path: _path }] }`,
1188
+ ` }`,
1189
+ ` return true`,
1190
+ `}`
1191
+ ].join("\n");
1744
1192
  }
1745
- return [`export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`, ` return true`, `}`].join('\n');
1193
+ const hoistedBlock = rootCtx.hoisted.length > 0 ? `${rootCtx.hoisted.join("\n")}
1194
+
1195
+ ` : "";
1196
+ return [
1197
+ `${hoistedBlock}export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1198
+ ` if (${wrongType}) {`,
1199
+ ` return { valid: false, errors: [{ message: 'must be ${typLabel}', path: _path }] }`,
1200
+ ` }`,
1201
+ ` let errors: ValidationError[] | undefined`,
1202
+ constraintLines.join("\n").replaceAll("errors.push(", "(errors ??= []).push("),
1203
+ ` return errors !== undefined ? { valid: false, errors } : true`,
1204
+ `}`
1205
+ ].join("\n");
1206
+ }
1207
+ return [`export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`, ` return true`, `}`].join("\n");
1746
1208
  };
1747
- /**
1748
- * Throws when a schema (anywhere in its subtree) uses a keyword this generator
1749
- * does not implement but which *narrows* the set of valid documents. Today that
1750
- * is `unevaluatedProperties` / `unevaluatedItems` with a constraining value
1751
- * (`false` or a subschema). The generator has no support for them — only the
1752
- * runtime interpreter does — so silently emitting a validator would produce one
1753
- * that ACCEPTS documents the interpreter REJECTS: a wrong verdict, worse than an
1754
- * error. `unevaluated*: true` is a no-op (it permits everything), so it is
1755
- * allowed through.
1756
- *
1757
- * We deliberately throw rather than implement the keywords: doing them correctly
1758
- * requires tracking which properties/items each combinator branch "evaluated",
1759
- * which is a large, separate feature. Failing loudly at generation time surfaces
1760
- * the gap instead of shipping a validator that lies.
1761
- */
1762
1209
  const assertNoUnsupportedKeywords = (schema, typeName) => {
1763
- const visit = (node) => {
1764
- if (typeof node !== 'object' || node === null)
1765
- return;
1766
- if (Array.isArray(node)) {
1767
- for (const item of node)
1768
- visit(item);
1769
- return;
1770
- }
1771
- const record = node;
1772
- for (const keyword of ['unevaluatedProperties', 'unevaluatedItems']) {
1773
- // `true` permits everything no constraint safe to ignore.
1774
- if (keyword in record && record[keyword] !== true) {
1775
- throw new Error(`[${typeName}] unsupported keyword "${keyword}": the validator generator does not implement it and would ` +
1776
- `silently accept documents the interpreter rejects. Validate this schema with the runtime interpreter, ` +
1777
- `or remove the keyword.`);
1778
- }
1779
- }
1780
- for (const value of Object.values(record))
1781
- visit(value);
1782
- };
1783
- visit(schema);
1210
+ const visit = (node) => {
1211
+ if (typeof node !== "object" || node === null)
1212
+ return;
1213
+ if (Array.isArray(node)) {
1214
+ for (const item of node)
1215
+ visit(item);
1216
+ return;
1217
+ }
1218
+ const record = node;
1219
+ for (const keyword of ["unevaluatedProperties", "unevaluatedItems"]) {
1220
+ if (keyword in record && record[keyword] !== true) {
1221
+ throw new Error(`[${typeName}] unsupported keyword "${keyword}": the validator generator does not implement it and would silently accept documents the interpreter rejects. Validate this schema with the runtime interpreter, or remove the keyword.`);
1222
+ }
1223
+ }
1224
+ for (const value of Object.values(record))
1225
+ visit(value);
1226
+ };
1227
+ visit(schema);
1784
1228
  };
1785
- /** Keywords whose value is a single subschema (or a boolean schema). */
1786
- const SINGLE_SUBSCHEMA_KEYS = new Set([
1787
- 'additionalProperties',
1788
- 'additionalItems',
1789
- 'contains',
1790
- 'propertyNames',
1791
- 'not',
1792
- 'if',
1793
- 'then',
1794
- 'else',
1795
- 'unevaluatedProperties',
1796
- 'unevaluatedItems',
1229
+ const SINGLE_SUBSCHEMA_KEYS = /* @__PURE__ */ new Set([
1230
+ "additionalProperties",
1231
+ "additionalItems",
1232
+ "contains",
1233
+ "propertyNames",
1234
+ "not",
1235
+ "if",
1236
+ "then",
1237
+ "else",
1238
+ "unevaluatedProperties",
1239
+ "unevaluatedItems"
1797
1240
  ]);
1798
- /** Keywords whose value is an array of subschemas. */
1799
- const SUBSCHEMA_LIST_KEYS = new Set(['allOf', 'anyOf', 'oneOf', 'prefixItems']);
1800
- /** Keywords whose value is a map of names to subschemas. */
1801
- const SUBSCHEMA_MAP_KEYS = new Set(['properties', 'patternProperties', 'dependentSchemas', '$defs', 'definitions']);
1802
- /**
1803
- * Rewrites OpenAPI 3.0 `nullable: true` into a form the generator already
1804
- * enforces. A node `{ nullable: true, ...rest }` accepts a value iff the value is
1805
- * `null` OR it matches `rest` — exactly `{ anyOf: [{ type: 'null' }, rest] }`.
1806
- * Emitting that lets the existing `anyOf` machinery (and its guard bail-outs)
1807
- * mirror the interpreter, which short-circuits every keyword when a `nullable`
1808
- * node sees `null`.
1809
- *
1810
- * The walk descends only into genuine subschema positions (never into `enum` /
1811
- * `const` / `default` data), so a data value that merely looks like a schema is
1812
- * never rewritten. Returns a fresh tree; the caller's schema is untouched.
1813
- */
1241
+ const SUBSCHEMA_LIST_KEYS = /* @__PURE__ */ new Set(["allOf", "anyOf", "oneOf", "prefixItems"]);
1242
+ const SUBSCHEMA_MAP_KEYS = /* @__PURE__ */ new Set(["properties", "patternProperties", "dependentSchemas", "$defs", "definitions"]);
1814
1243
  const rewriteNullable = (node) => {
1815
- if (typeof node !== 'object' || node === null || Array.isArray(node))
1816
- return node;
1817
- const src = node;
1818
- const out = {};
1819
- for (const [key, value] of Object.entries(src)) {
1820
- if (key === 'nullable')
1821
- continue; // folded into the `anyOf` wrapper below
1822
- if (SINGLE_SUBSCHEMA_KEYS.has(key)) {
1823
- out[key] = rewriteNullable(value);
1824
- }
1825
- else if (key === 'items') {
1826
- // `items` is either a single subschema (2020-12) or a tuple array (draft).
1827
- out[key] = Array.isArray(value) ? value.map(rewriteNullable) : rewriteNullable(value);
1828
- }
1829
- else if (SUBSCHEMA_LIST_KEYS.has(key) && Array.isArray(value)) {
1830
- out[key] = value.map(rewriteNullable);
1831
- }
1832
- else if (SUBSCHEMA_MAP_KEYS.has(key) && typeof value === 'object' && value !== null) {
1833
- const mapped = {};
1834
- for (const [name, sub] of Object.entries(value))
1835
- mapped[name] = rewriteNullable(sub);
1836
- out[key] = mapped;
1837
- }
1838
- else if (key === 'dependencies' && typeof value === 'object' && value !== null) {
1839
- // Dual-form: an array value lists required keys (data), a schema value is a
1840
- // subschema.
1841
- const mapped = {};
1842
- for (const [name, sub] of Object.entries(value)) {
1843
- mapped[name] = Array.isArray(sub) ? sub : rewriteNullable(sub);
1844
- }
1845
- out[key] = mapped;
1846
- }
1847
- else {
1848
- out[key] = value;
1849
- }
1850
- }
1851
- if (src['nullable'] === true)
1852
- return { anyOf: [{ type: 'null' }, out] };
1853
- return out;
1244
+ if (typeof node !== "object" || node === null || Array.isArray(node))
1245
+ return node;
1246
+ const src = node;
1247
+ const out = {};
1248
+ for (const [key, value] of Object.entries(src)) {
1249
+ if (key === "nullable")
1250
+ continue;
1251
+ if (SINGLE_SUBSCHEMA_KEYS.has(key)) {
1252
+ out[key] = rewriteNullable(value);
1253
+ } else if (key === "items") {
1254
+ out[key] = Array.isArray(value) ? value.map(rewriteNullable) : rewriteNullable(value);
1255
+ } else if (SUBSCHEMA_LIST_KEYS.has(key) && Array.isArray(value)) {
1256
+ out[key] = value.map(rewriteNullable);
1257
+ } else if (SUBSCHEMA_MAP_KEYS.has(key) && typeof value === "object" && value !== null) {
1258
+ const mapped = {};
1259
+ for (const [name, sub] of Object.entries(value))
1260
+ mapped[name] = rewriteNullable(sub);
1261
+ out[key] = mapped;
1262
+ } else if (key === "dependencies" && typeof value === "object" && value !== null) {
1263
+ const mapped = {};
1264
+ for (const [name, sub] of Object.entries(value)) {
1265
+ mapped[name] = Array.isArray(sub) ? sub : rewriteNullable(sub);
1266
+ }
1267
+ out[key] = mapped;
1268
+ } else {
1269
+ out[key] = value;
1270
+ }
1271
+ }
1272
+ if (src["nullable"] === true)
1273
+ return { anyOf: [{ type: "null" }, out] };
1274
+ return out;
1854
1275
  };
1855
- /**
1856
- * Generates a TypeScript validator function from a JSON Schema.
1857
- *
1858
- * The generated function accepts `unknown` input and returns `true` if valid,
1859
- * or `{ valid: false, errors }` with a list of errors if not.
1860
- *
1861
- * Object schemas check that required properties are present and that all
1862
- * provided properties match their declared types. Non-object schemas (strings,
1863
- * numbers, enums, $refs) emit an inline type check.
1864
- *
1865
- * @example
1866
- * ```typescript
1867
- * generateValidatorFunction({ type: 'object', properties: { name: { type: 'string' } }, required: ['name'] }, 'Info')
1868
- * // export const validateInfo = (input: unknown, _path = ''): ValidationResult => {
1869
- * // if (typeof input !== 'object' || ...) return { valid: false, ... }
1870
- * // const errors: ValidationError[] = []
1871
- * // const obj = input as Record<string, unknown>
1872
- * // if (!('name' in obj)) { errors.push(...) } else if (typeof obj['name'] !== 'string') { errors.push(...) }
1873
- * // return errors.length > 0 ? { valid: false, errors } : true
1874
- * // }
1875
- * ```
1876
- */
1877
- export const generateValidatorFunction = (schema, typeName, suffix = '') => {
1878
- assertNoUnsupportedKeywords(schema, typeName);
1879
- // Fold OpenAPI `nullable: true` into the `anyOf` form the generator already
1880
- // enforces, so the emitted checks match the interpreter's null short-circuit.
1881
- const rewritten = rewriteNullable(schema);
1882
- if (isObjectSchema(rewritten)) {
1883
- return generateObjectValidator(rewritten, typeName, suffix);
1884
- }
1885
- return generateScalarValidator(rewritten, typeName, suffix);
1276
+ const generateValidatorFunction = (schema, typeName, suffix = "") => {
1277
+ assertNoUnsupportedKeywords(schema, typeName);
1278
+ const rewritten = rewriteNullable(schema);
1279
+ if (isObjectSchema(rewritten)) {
1280
+ return generateObjectValidator(rewritten, typeName, suffix);
1281
+ }
1282
+ return generateScalarValidator(rewritten, typeName, suffix);
1283
+ };
1284
+ export {
1285
+ generateBooleanGuard,
1286
+ generateValidatorFunction
1886
1287
  };