ata-validator 1.9.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -193,11 +193,13 @@ function compileToJS(schema, defs, schemaMap) {
193
193
  // Collect $defs early so sub-schemas can resolve $ref
194
194
  const rootDefs = defs || collectDefs(schema)
195
195
 
196
- // Bail on features that are too complex for JS fast path
197
- if (schema.patternProperties ||
198
- schema.dependentSchemas ||
199
- schema.propertyDependencies ||
200
- schema.propertyNames) {
196
+ // Bail on features this path has no emitter for. Tested for presence, not
197
+ // truthiness: `propertyNames: false` is falsy, passed this bail, and then
198
+ // validated nothing, which the entry-point agreement test caught.
199
+ if (schema.patternProperties !== undefined ||
200
+ schema.dependentSchemas !== undefined ||
201
+ schema.propertyDependencies !== undefined ||
202
+ schema.propertyNames !== undefined) {
201
203
  return null
202
204
  }
203
205
 
@@ -328,7 +330,7 @@ function compileToJS(schema, defs, schemaMap) {
328
330
  }
329
331
 
330
332
  // items, applied after the prefixItems positions
331
- if (schema.items) {
333
+ if (schema.items !== undefined && schema.items !== true) {
332
334
  const itemCheck = compileToJS(schema.items, rootDefs)
333
335
  if (!itemCheck) return null
334
336
  const start = Array.isArray(schema.prefixItems) ? schema.prefixItems.length : 0
@@ -359,7 +361,7 @@ function compileToJS(schema, defs, schemaMap) {
359
361
  }
360
362
 
361
363
  // contains
362
- if (schema.contains) {
364
+ if (schema.contains !== undefined) {
363
365
  const containsCheck = compileToJS(schema.contains, rootDefs)
364
366
  if (!containsCheck) return null
365
367
  const minC = schema.minContains !== undefined ? schema.minContains : 1
@@ -517,20 +519,20 @@ function compileToJS(schema, defs, schemaMap) {
517
519
  }
518
520
 
519
521
  // not
520
- if (schema.not) {
522
+ if (schema.not !== undefined) {
521
523
  const notFn = compileToJS(schema.not, rootDefs)
522
524
  if (!notFn) return null
523
525
  checks.push((d) => !notFn(d))
524
526
  }
525
527
 
526
528
  // if/then/else
527
- if (schema.if) {
529
+ if (schema.if !== undefined) {
528
530
  const ifFn = compileToJS(schema.if, rootDefs)
529
531
  if (!ifFn) return null
530
- const thenFn = schema.then ? compileToJS(schema.then, rootDefs) : null
531
- const elseFn = schema.else ? compileToJS(schema.else, rootDefs) : null
532
- if (schema.then && !thenFn) return null
533
- if (schema.else && !elseFn) return null
532
+ const thenFn = schema.then !== undefined ? compileToJS(schema.then, rootDefs) : null
533
+ const elseFn = schema.else !== undefined ? compileToJS(schema.else, rootDefs) : null
534
+ if (schema.then !== undefined && !thenFn) return null
535
+ if (schema.else !== undefined && !elseFn) return null
534
536
  checks.push((d) => {
535
537
  if (ifFn(d)) {
536
538
  return thenFn ? thenFn(d) : true
@@ -756,12 +758,13 @@ function _canonical(x) {
756
758
  return '{' + Object.keys(x).sort().map((k) => JSON.stringify(k) + ':' + _canonical(x[k])).join(',') + '}'
757
759
  }
758
760
 
761
+ const _formats = require('./formats')
759
762
  const FORMAT_CHECKS = {
760
763
  email: (s) => { const at = s.indexOf('@'); return at > 0 && at < s.length - 1 && s.indexOf('.', at) > at + 1 },
761
- date: (s) => { if (s.length !== 10 || !/^\d{4}-\d{2}-\d{2}$/.test(s)) return false; const m = +s.slice(5, 7), d = +s.slice(8, 10); return m >= 1 && m <= 12 && d >= 1 && d <= 31 },
764
+ date: _formats.date,
762
765
  uuid: (s) => s.length === 36 && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s),
763
- ipv4: (s) => { const p = s.split('.'); return p.length === 4 && p.every(n => { const v = +n; return v >= 0 && v <= 255 && String(v) === n }) },
764
- hostname: (s) => s.length > 0 && s.length <= 253 && /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i.test(s),
766
+ ipv4: _formats.ipv4,
767
+ hostname: _formats.hostname,
765
768
  }
766
769
 
767
770
  // Dangerous JS property names that exist on Object.prototype
@@ -842,6 +845,94 @@ function hasNestedIdScope(node, isRoot = true) {
842
845
 
843
846
  // Recursively check if a schema can be safely compiled to JS codegen.
844
847
  // Returns false if any sub-schema contains features codegen gets wrong.
848
+ // Names of the defs that sit on a $ref cycle among defs. Such a def compiles
849
+ // as a named function rather than being inlined, so recursion terminates in
850
+ // the generated code the same way `$ref: "#"` does at the root. A plain
851
+ // cross-reference (A -> B where B does not refer back) is not on a cycle and
852
+ // still inlines.
853
+ // Data can point back at itself, and a schema that references itself would
854
+ // then recurse until the stack runs out. Validation runs in two passes: the
855
+ // fast one counts depth and gives up past CYCLE_DEPTH, the second re-runs with
856
+ // a set of the values currently under check, where a value met twice is a
857
+ // fixed point and counts as satisfied. That is what the interpreted engine
858
+ // does, so both engines answer the same on cyclic input. Legitimate documents
859
+ // never reach the threshold, so they only pay the counter.
860
+ // Raised by a generator that meets a construct it cannot express. The entry
861
+ // point turns it into a decline, so the caller falls back to an engine that
862
+ // can. Emitting nothing would mean "everything is valid", which is the failure
863
+ // mode this project has shipped three times in $ref handling; declining is the
864
+ // only safe way out.
865
+ const DECLINE = Symbol('ata.decline')
866
+
867
+ const CYCLE_DEPTH = 512
868
+
869
+ // Declarations the guarded functions close over. Emitted once, ahead of them.
870
+ function emitGuardState () {
871
+ return 'const _CYC={};const _stk=new Set();let _sd=0,_sg=false\n '
872
+ }
873
+
874
+ // Wraps a self-recursive root: `name` is the guarded entry, `name`_b the body.
875
+ function emitRootGuard (name, args, onRepeat) {
876
+ const call = `${name}_b(${args})`
877
+ return (
878
+ `function ${name}(${args}){\n ` +
879
+ `if(_sg){if(typeof d!=='object'||d===null)return ${call};if(_stk.has(d))return ${onRepeat};_stk.add(d);try{return ${call}}finally{_stk.delete(d)}}\n ` +
880
+ `if(++_sd>${CYCLE_DEPTH})throw _CYC\n const _r=${call}\n _sd--\n return _r\n }\n `
881
+ )
882
+ }
883
+
884
+ // Runs `attempt`, and on the depth signal runs it again under the guard.
885
+ // `reset` clears anything the abandoned pass left behind.
886
+ function emitGuardedRun (attempt, reset, defSets) {
887
+ const clears = ['_stk.clear()'].concat(defSets.map((n) => `${n}_s.clear()`)).join(';')
888
+ return (
889
+ `_sd=0\n try{${attempt}}catch(_err){\n ` +
890
+ `if(_err!==_CYC)throw _err\n ` +
891
+ `${reset}_sg=true;${clears}\n ` +
892
+ `try{${attempt}}finally{_sg=false}\n }\n `
893
+ )
894
+ }
895
+
896
+ function cyclicDefNames (defs) {
897
+ const out = new Set()
898
+ if (!defs || typeof defs !== 'object') return out
899
+ const defNames = new Set(Object.keys(defs))
900
+ const defRefs = {}
901
+ for (const [name, def] of Object.entries(defs)) {
902
+ const refs = []
903
+ const seen = new WeakSet()
904
+ const collectRefs = (node) => {
905
+ if (typeof node !== 'object' || node === null) return
906
+ if (seen.has(node)) return
907
+ seen.add(node)
908
+ if (node.$ref) {
909
+ const m = /^#\/(?:\$defs|definitions)\/([^/]+)$/.exec(node.$ref)
910
+ if (m && defNames.has(m[1])) refs.push(m[1])
911
+ }
912
+ for (const val of Object.values(node)) {
913
+ if (typeof val === 'object' && val !== null) {
914
+ if (Array.isArray(val)) { for (const item of val) collectRefs(item) } else collectRefs(val)
915
+ }
916
+ }
917
+ }
918
+ collectRefs(def)
919
+ defRefs[name] = refs
920
+ }
921
+ const visited = new Set()
922
+ const inStack = []
923
+ const dfs = (node) => {
924
+ const at = inStack.indexOf(node)
925
+ if (at !== -1) { for (let i = at; i < inStack.length; i++) out.add(inStack[i]); return }
926
+ if (visited.has(node)) return
927
+ visited.add(node)
928
+ inStack.push(node)
929
+ for (const neighbor of (defRefs[node] || [])) dfs(neighbor)
930
+ inStack.pop()
931
+ }
932
+ for (const name of Object.keys(defRefs)) dfs(name)
933
+ return out
934
+ }
935
+
845
936
  function codegenSafe(schema, schemaMap) {
846
937
  if (typeof schema === 'boolean') return true
847
938
  if (typeof schema !== 'object' || schema === null) return true
@@ -855,13 +946,12 @@ function codegenSafe(schema, schemaMap) {
855
946
  if (schema.$dynamicRef && !schema.$dynamicRef.startsWith('#')) return false
856
947
 
857
948
  // Boolean sub-schemas anywhere cause bail — codegen doesn't handle schema=false correctly
858
- if (schema.items === false) return false
859
- if (schema.items === true && !schema.unevaluatedItems) return false
949
+ // items: false and items: true are constants; every generator handles a
950
+ // boolean child at its head, so neither falls through as "nothing".
860
951
  if (schema.additionalProperties === true) return true // permissive — fine
861
952
  if (schema.properties) {
862
953
  for (const v of Object.values(schema.properties)) {
863
- if (v === false) return false // property: false is complex
864
- if (v === true) continue // property: true is always valid
954
+ if (typeof v === 'boolean') continue // constants; every generator handles them at its head
865
955
  if (!codegenSafe(v, schemaMap)) return false
866
956
  }
867
957
  }
@@ -926,15 +1016,17 @@ function codegenSafe(schema, schemaMap) {
926
1016
  // additionalProperties as schema — supported when no composition (allOf/oneOf/anyOf)
927
1017
  // is in play, and no patternProperties (unified loop does not emit the
928
1018
  // per-key sub-schema walk yet). Closure path handles those cases.
1019
+ // additionalProperties as a schema: emitted by the per-key walk, or by the
1020
+ // unified key loop when patternProperties is present. Composition alongside
1021
+ // it is fine, since additionalProperties only looks at this schema's own
1022
+ // properties and patternProperties.
929
1023
  if (typeof schema.additionalProperties === 'object' && schema.additionalProperties !== null) {
930
- if (schema.allOf || schema.oneOf || schema.anyOf) return false
931
- if (schema.patternProperties) return false
932
1024
  if (!codegenSafe(schema.additionalProperties, schemaMap)) return false
933
1025
  }
934
1026
  if (schema.additionalProperties === false && !schema.properties) return false
935
1027
 
936
1028
  // propertyNames: false — codegen doesn't handle this
937
- if (schema.propertyNames === false) return false
1029
+ // propertyNames: false is emitted as a per-key rejection by every generator.
938
1030
 
939
1031
  // unevaluatedProperties: allow boolean and schema values
940
1032
  if (schema.unevaluatedProperties !== undefined) {
@@ -967,53 +1059,8 @@ function codegenSafe(schema, schemaMap) {
967
1059
  const defs = schema.$defs || schema.definitions
968
1060
  if (defs) {
969
1061
  const defNames = new Set(Object.keys(defs))
970
- // Detect cyclic local refs: build a directed graph of def->def references and
971
- // check for cycles using DFS. A plain cross-reference (A -> B where B doesn't
972
- // refer back to A) is fine; only true cycles (A -> B -> A) must be rejected.
973
- // The JS codegen returns (() => true) on cycle (permissive), so cyclic schemas
974
- // must route to the interpreted engine instead.
975
- const defRefs = {}
976
- for (const [name, def] of Object.entries(defs)) {
977
- defRefs[name] = []
978
- if (typeof def === 'object' && def !== null) {
979
- const refs = []
980
- const seen = new WeakSet()
981
- const collectRefs = (node) => {
982
- if (typeof node !== 'object' || node === null) return
983
- if (seen.has(node)) return
984
- seen.add(node)
985
- if (node.$ref) {
986
- const m = /^#\/(?:\$defs|definitions)\/([^/]+)$/.exec(node.$ref)
987
- if (m && defNames.has(m[1])) refs.push(m[1])
988
- }
989
- for (const val of Object.values(node)) {
990
- if (typeof val === 'object' && val !== null) {
991
- if (Array.isArray(val)) { for (const item of val) collectRefs(item) }
992
- else collectRefs(val)
993
- }
994
- }
995
- }
996
- collectRefs(def)
997
- defRefs[name] = refs
998
- }
999
- }
1000
- const hasCycle = (() => {
1001
- const visited = new Set()
1002
- const inStack = new Set()
1003
- const dfs = (node) => {
1004
- if (inStack.has(node)) return true
1005
- if (visited.has(node)) return false
1006
- visited.add(node)
1007
- inStack.add(node)
1008
- for (const neighbor of (defRefs[node] || [])) {
1009
- if (dfs(neighbor)) return true
1010
- }
1011
- inStack.delete(node)
1012
- return false
1013
- }
1014
- return Object.keys(defRefs).some(dfs)
1015
- })()
1016
- if (hasCycle) return false
1062
+ // Defs on a $ref cycle compile as named functions (see genCode); nothing
1063
+ // to decline here. cyclicDefNames() is the walk that used to bail.
1017
1064
  for (const [name, def] of Object.entries(defs)) {
1018
1065
  if (/[~/"']/.test(name)) return false // special chars in def name
1019
1066
  if (typeof def === 'boolean') return false
@@ -1030,6 +1077,10 @@ function codegenSafe(schema, schemaMap) {
1030
1077
  }
1031
1078
 
1032
1079
  // Recurse into sub-schemas — bail on boolean schemas in any position
1080
+ // A boolean subschema is a constant that every generator emits at its
1081
+ // head with the child's own path. The emitters below test for presence,
1082
+ // not truthiness, because `false` is falsy and a truthiness test would
1083
+ // skip it and validate nothing.
1033
1084
  const subs = [
1034
1085
  schema.items, schema.contains, schema.not,
1035
1086
  schema.if, schema.then, schema.else,
@@ -1040,9 +1091,7 @@ function codegenSafe(schema, schemaMap) {
1040
1091
  ]
1041
1092
  if (typeof schema.additionalProperties === 'object') subs.push(schema.additionalProperties)
1042
1093
  for (const s of subs) {
1043
- if (s === undefined || s === null) continue
1044
- if (s === false) return false // boolean false sub-schema — complex
1045
- if (s === true) continue // boolean true sub-schema — always valid, fine
1094
+ if (s === undefined || s === null || typeof s === 'boolean') continue
1046
1095
  if (!codegenSafe(s, schemaMap)) return false
1047
1096
  }
1048
1097
 
@@ -1330,7 +1379,6 @@ function compileToJSCodegen(schema, schemaMap, userFormats) {
1330
1379
  // patternProperties: bail only on boolean sub-schemas or unicode property escapes
1331
1380
  if (schema.patternProperties) {
1332
1381
  for (const [pat, sub] of Object.entries(schema.patternProperties)) {
1333
- if (typeof sub === 'boolean') return null
1334
1382
  if (/\\[pP]\{/.test(pat)) return null
1335
1383
  if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
1336
1384
  }
@@ -1338,13 +1386,11 @@ function compileToJSCodegen(schema, schemaMap, userFormats) {
1338
1386
  // dependentSchemas: bail on boolean sub-schemas
1339
1387
  if (schema.dependentSchemas) {
1340
1388
  for (const sub of Object.values(schema.dependentSchemas)) {
1341
- if (typeof sub === 'boolean') return null
1342
1389
  if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
1343
1390
  }
1344
1391
  }
1345
1392
  // propertyNames: only codegen simple string constraints
1346
1393
  if (schema.propertyNames) {
1347
- if (typeof schema.propertyNames === 'boolean') return null
1348
1394
  const pn = schema.propertyNames
1349
1395
  const supported = ['maxLength', 'minLength', 'pattern', 'const', 'enum']
1350
1396
  const keys = Object.keys(pn).filter(k => k !== '$schema')
@@ -1414,18 +1460,33 @@ function compileToJSCodegen(schema, schemaMap, userFormats) {
1414
1460
  }
1415
1461
  }
1416
1462
 
1463
+ const defSets = ctx.defFns ? Array.from(ctx.defFns.values()) : []
1464
+ const needsGuard = ctx.usesRecursion || defSets.length > 0
1465
+
1417
1466
  let body, hybridBody
1418
1467
  if (ctx.usesRecursion) {
1419
- // Self-recursive: wrap in named function
1420
- body = `function _validate(d){\n ${checkStr}\n return true\n }\n return _validate(d)`
1421
- // Hybrid: keep _validate as boolean, wrap only the outer call
1422
- hybridBody = `function _validate(d){\n ${checkStr}\n return true\n }\n return _validate(d)?R:E(d)`
1468
+ // Self-recursive: a named body reached through the cycle guard, so data
1469
+ // that points back at itself settles instead of exhausting the stack.
1470
+ const decl = emitRootGuard('_validate', 'd', 'true') +
1471
+ `function _validate_b(d){\n ${checkStr}\n return true\n }\n `
1472
+ const run = emitGuardedRun('return _validate(d)', '', defSets)
1473
+ body = `${decl}function _run(d){\n ${run}\n }\n return _run(d)`
1474
+ // Hybrid: keep the boolean answer, wrap only the outer call
1475
+ hybridBody = `${decl}function _run(d){\n ${run}\n }\n return _run(d)?R:E(d)`
1476
+ } else if (defSets.length > 0) {
1477
+ // No self-reference at the root, but a def on a cycle can still be
1478
+ // re-entered with the same value; the guarded functions need their state.
1479
+ const run = emitGuardedRun(`return _body(d)`, '', defSets)
1480
+ body = `function _body(d){\n ${checkStr}\n return true\n }\n function _run(d){\n ${run}\n }\n return _run(d)`
1481
+ hybridBody = `function _body(d){\n ${checkStr}\n return true\n }\n function _run(d){\n ${run}\n }\n return _run(d)?R:E(d)`
1423
1482
  } else {
1424
1483
  body = checkStr + '\n return true'
1425
1484
  hybridBody = replaceTopLevel(checkStr + '\n return R')
1426
1485
  }
1427
1486
 
1428
- const preambleStr = ctx.preamble && ctx.preamble.length ? ctx.preamble.join('\n ') + '\n ' : ''
1487
+ const guardStr = needsGuard ? emitGuardState() : ''
1488
+ const preambleStr = guardStr +
1489
+ (ctx.preamble && ctx.preamble.length ? ctx.preamble.join('\n ') + '\n ' : '')
1429
1490
 
1430
1491
  try {
1431
1492
  let boolFn
@@ -1654,6 +1715,11 @@ function _deferOrInline(ctx, lines, v, check) {
1654
1715
  // 'array' = we know v is an array
1655
1716
  // 'string' / 'number' / 'integer' = we know the primitive type
1656
1717
  function genCode(schema, v, lines, ctx, knownType) {
1718
+ // A boolean schema is a constant. false rejects whatever reached it; true
1719
+ // constrains nothing and emits nothing, which is correct only because the
1720
+ // caller's own checks still stand around this call.
1721
+ if (schema === false) { lines.push('return false'); return }
1722
+ if (schema === true) return
1657
1723
  if (typeof schema !== 'object' || schema === null) return
1658
1724
  if (!ctx.regExpMap) {
1659
1725
  ctx.regExpMap = new Map();
@@ -1673,10 +1739,37 @@ function genCode(schema, v, lines, ctx, knownType) {
1673
1739
  // 1. Local ref
1674
1740
  const m = schema.$ref !== '#' && schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
1675
1741
  if (m && ctx.rootDefs && ctx.rootDefs[m[1]]) {
1676
- if (ctx.refStack.has(schema.$ref)) { if (!hasSiblings) return }
1742
+ const defName = m[1]
1743
+ if (!ctx.cyclicDefs) ctx.cyclicDefs = cyclicDefNames(ctx.rootDefs)
1744
+ if (ctx.cyclicDefs.has(defName) && ctx.preamble) {
1745
+ // A def on a cycle is a named function in the preamble, emitted once
1746
+ // and called by name, so recursion terminates the way `$ref: "#"`
1747
+ // does at the root. The name is registered before the body is
1748
+ // generated so a self-reference inside it becomes a call.
1749
+ if (!ctx.defFns) ctx.defFns = new Map()
1750
+ let fnName = ctx.defFns.get(defName)
1751
+ if (!fnName) {
1752
+ fnName = '_def' + ctx.defFns.size + '_' + defName.replace(/[^A-Za-z0-9_]/g, '_')
1753
+ ctx.defFns.set(defName, fnName)
1754
+ const bodyLines = []
1755
+ genCode(ctx.rootDefs[defName], 'd', bodyLines, ctx)
1756
+ // Entered through the cycle guard: see emitGuardState. In the fast
1757
+ // pass the wrapper only counts depth; the set is used by the second,
1758
+ // guarded pass that runs when data turns out to point back at
1759
+ // itself.
1760
+ ctx.preamble.push(
1761
+ `const ${fnName}_s=new Set()\n function ${fnName}(d){\n ` +
1762
+ `if(_sg){if(typeof d!=='object'||d===null)return ${fnName}_b(d);if(${fnName}_s.has(d))return true;${fnName}_s.add(d);try{return ${fnName}_b(d)}finally{${fnName}_s.delete(d)}}\n ` +
1763
+ `if(++_sd>${CYCLE_DEPTH})throw _CYC\n const _r=${fnName}_b(d)\n _sd--\n return _r\n }\n ` +
1764
+ `function ${fnName}_b(d){${bodyLines.join('\n ')}\n return true}`,
1765
+ )
1766
+ }
1767
+ lines.push(`if(!${fnName}(${v}))return false`)
1768
+ if (!hasSiblings) return
1769
+ } else if (ctx.refStack.has(schema.$ref)) { if (!hasSiblings) return }
1677
1770
  else {
1678
1771
  ctx.refStack.add(schema.$ref)
1679
- genCode(ctx.rootDefs[m[1]], v, lines, ctx, knownType)
1772
+ genCode(ctx.rootDefs[defName], v, lines, ctx, knownType)
1680
1773
  ctx.refStack.delete(schema.$ref)
1681
1774
  if (!hasSiblings) return
1682
1775
  }
@@ -2048,11 +2141,23 @@ function genCode(schema, v, lines, ctx, knownType) {
2048
2141
 
2049
2142
  const guard = isObj ? '' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`
2050
2143
 
2051
- if (schema.additionalProperties === false && schema.properties) {
2052
- // Unified loop: properties + patterns + propertyNames + additionalProperties
2144
+ const apSchema = typeof schema.additionalProperties === 'object' && schema.additionalProperties !== null ? schema.additionalProperties : null
2145
+ if ((schema.additionalProperties === false && schema.properties) || apSchema) {
2146
+ // Unified loop: properties + patterns + propertyNames + additionalProperties.
2147
+ // A key that is neither declared nor matched by any pattern is
2148
+ // additional: rejected outright, or run through the additionalProperties
2149
+ // schema when there is one.
2053
2150
  ctx._ppHandledAdditional = true
2054
2151
  ctx._ppHandledPropertyNames = !!pn
2055
- const propKeys = Object.keys(schema.properties)
2152
+ const propKeys = Object.keys(schema.properties || {})
2153
+ let apFn = null
2154
+ if (apSchema) {
2155
+ const apLines = []
2156
+ genCode(apSchema, '_apv', apLines, ctx)
2157
+ apFn = `_apf${pi}`
2158
+ ctx.closureVars.push(apFn)
2159
+ ctx.closureVals.push(new Function('_apv', apLines.length === 0 ? 'return true' : `${apLines.join(';')};return true`))
2160
+ }
2056
2161
  lines.push(`${guard}{for(const ${kVar} in ${v}){`)
2057
2162
  // propertyNames checks (merged into same loop)
2058
2163
  if (pn) {
@@ -2077,21 +2182,29 @@ function genCode(schema, v, lines, ctx, knownType) {
2077
2182
  lines.push(`if(!_es${ei}.has(${kVar}))return false`)
2078
2183
  }
2079
2184
  }
2080
- // Check: is key declared or matches a pattern?
2081
- // switch/case: V8 compiles string cases to jump table (faster than chained ===)
2082
- const switchCases = propKeys.map(k => `case ${JSON.stringify(k)}:`).join('')
2083
- lines.push(`switch(${kVar}){${switchCases}break;default:`)
2084
- // Default: key is not declared — must match a pattern
2085
- let patternChecks = []
2086
- for (let i = 0; i < ppEntries.length; i++) {
2087
- patternChecks.push(`if(${matchers[i].check}){if(!_ppf${pi}_${i}(${v}[${kVar}]))return false}else{return false}`)
2185
+ // Every matching pattern applies to every key, declared or not: a
2186
+ // declared key that also matches a pattern is validated by both (the
2187
+ // properties emitter handles its own subschema). A flag rather than an
2188
+ // if/else chain, because a chain returned false at the first pattern
2189
+ // that missed and wrongly rejected a key only a later pattern matched.
2190
+ if (ppEntries.length > 0) {
2191
+ lines.push(`let _pm${pi}=false`)
2192
+ for (let i = 0; i < ppEntries.length; i++) {
2193
+ lines.push(`if(${matchers[i].check}){_pm${pi}=true;if(!_ppf${pi}_${i}(${v}[${kVar}]))return false}`)
2194
+ }
2088
2195
  }
2089
- if (patternChecks.length > 0) {
2090
- lines.push(patternChecks.join(''))
2196
+ // A key that is neither declared nor matched is additional. switch on
2197
+ // the declared names (V8 compiles string cases to a jump table); no
2198
+ // switch at all when nothing is declared, since a switch with no case
2199
+ // clause is a syntax error.
2200
+ const additional = apFn ? `if(!${apFn}(${v}[${kVar}]))return false` : `return false`
2201
+ const notMatched = ppEntries.length > 0 ? `if(!_pm${pi}){${additional}}` : additional
2202
+ if (propKeys.length) {
2203
+ const switchCases = propKeys.map(k => `case ${JSON.stringify(k)}:`).join('')
2204
+ lines.push(`switch(${kVar}){${switchCases}break;default:{${notMatched}}}`)
2091
2205
  } else {
2092
- lines.push(`return false`)
2206
+ lines.push(notMatched)
2093
2207
  }
2094
- lines.push(`}`) // end switch
2095
2208
  lines.push(`}}`)
2096
2209
  } else {
2097
2210
  // No additionalProperties: validate matching keys + propertyNames
@@ -2139,6 +2252,13 @@ function genCode(schema, v, lines, ctx, knownType) {
2139
2252
  }
2140
2253
  }
2141
2254
 
2255
+ // propertyNames: false rejects any key at all; true constrains nothing.
2256
+ if (schema.propertyNames === false) {
2257
+ lines.push(isObj
2258
+ ? `for(const _k in ${v})return false`
2259
+ : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k in ${v})return false}`)
2260
+ }
2261
+
2142
2262
  // propertyNames — only emit if not already merged into patternProperties loop
2143
2263
  if (schema.propertyNames && typeof schema.propertyNames === 'object' && !ctx._ppHandledPropertyNames) {
2144
2264
  const pn = schema.propertyNames
@@ -2207,7 +2327,7 @@ function genCode(schema, v, lines, ctx, knownType) {
2207
2327
  }
2208
2328
 
2209
2329
  // items — pass known type info to children
2210
- if (schema.items) {
2330
+ if (schema.items !== undefined && schema.items !== true) {
2211
2331
  const idx = `_j${ctx.varCounter}`
2212
2332
  const elem = `_e${ctx.varCounter}`
2213
2333
  ctx.varCounter++
@@ -2234,7 +2354,7 @@ function genCode(schema, v, lines, ctx, knownType) {
2234
2354
  }
2235
2355
 
2236
2356
  // contains — use helper function to avoid try/catch overhead
2237
- if (schema.contains) {
2357
+ if (schema.contains !== undefined) {
2238
2358
  const ci = ctx.varCounter++
2239
2359
  const minC = schema.minContains !== undefined ? schema.minContains : 1
2240
2360
  const maxC = schema.maxContains !== undefined ? schema.maxContains : Infinity
@@ -2314,7 +2434,7 @@ function genCode(schema, v, lines, ctx, knownType) {
2314
2434
  }
2315
2435
 
2316
2436
  // not
2317
- if (schema.not) {
2437
+ if (schema.not !== undefined) {
2318
2438
  const subLines = []
2319
2439
  genCode(schema.not, '_nv', subLines, ctx)
2320
2440
  if (subLines.length === 0) {
@@ -2326,7 +2446,7 @@ function genCode(schema, v, lines, ctx, knownType) {
2326
2446
  }
2327
2447
 
2328
2448
  // if/then/else
2329
- if (schema.if) {
2449
+ if (schema.if !== undefined) {
2330
2450
  const ifLines = []
2331
2451
  genCode(schema.if, '_iv', ifLines, ctx)
2332
2452
  const fi = ctx.varCounter++
@@ -2335,14 +2455,14 @@ function genCode(schema, v, lines, ctx, knownType) {
2335
2455
  : `function(_iv){${ifLines.join(';')};return true}`
2336
2456
 
2337
2457
  let thenFn = 'null', elseFn = 'null'
2338
- if (schema.then) {
2458
+ if (schema.then !== undefined) {
2339
2459
  const thenLines = []
2340
2460
  genCode(schema.then, '_tv', thenLines, ctx)
2341
2461
  thenFn = thenLines.length === 0
2342
2462
  ? `function(_tv){return true}`
2343
2463
  : `function(_tv){${thenLines.join(';')};return true}`
2344
2464
  }
2345
- if (schema.else) {
2465
+ if (schema.else !== undefined) {
2346
2466
  const elseLines = []
2347
2467
  genCode(schema.else, '_ev', elseLines, ctx)
2348
2468
  elseFn = elseLines.length === 0
@@ -2846,15 +2966,11 @@ const FORMAT_CODEGEN = {
2846
2966
  ? `{const _at=${v}.indexOf('@');if(_at<=0||_at>=${v}.length-1||${v}.indexOf('.',_at)<=_at+1)return false}`
2847
2967
  : `if(typeof ${v}==='string'){const _at=${v}.indexOf('@');if(_at<=0||_at>=${v}.length-1||${v}.indexOf('.',_at)<=_at+1)return false}`
2848
2968
  },
2849
- date: (v, isStr) => isStr
2850
- ? `{if(${v}.length!==10||!/^\\d{4}-\\d{2}-\\d{2}$/.test(${v}))return false;const _dm=+${v}.slice(5,7),_dd=+${v}.slice(8,10);if(_dm<1||_dm>12||_dd<1||_dd>31)return false}`
2851
- : `if(typeof ${v}==='string'){if(${v}.length!==10||!/^\\d{4}-\\d{2}-\\d{2}$/.test(${v}))return false;const _dm=+${v}.slice(5,7),_dd=+${v}.slice(8,10);if(_dm<1||_dm>12||_dd<1||_dd>31)return false}`,
2969
+ date: _formats.dateSource,
2852
2970
  uuid: (v, isStr) => isStr
2853
2971
  ? `if(${v}.length!==36||!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(${v}))return false`
2854
2972
  : `if(typeof ${v}==='string'&&(${v}.length!==36||!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(${v})))return false`,
2855
- 'date-time': (v, isStr) => isStr
2856
- ? `if(!/^\\d{4}-\\d{2}-\\d{2}[Tt]\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?([Zz]|[+-]\\d{2}:\\d{2})$/.test(${v})||isNaN(Date.parse(${v})))return false`
2857
- : `if(typeof ${v}==='string'&&(!/^\\d{4}-\\d{2}-\\d{2}[Tt]\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?([Zz]|[+-]\\d{2}:\\d{2})$/.test(${v})||isNaN(Date.parse(${v}))))return false`,
2973
+ 'date-time': _formats.dateTimeSource,
2858
2974
  time: (v, isStr) => isStr
2859
2975
  ? `if(!/^([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(\\.\\d+)?(Z|[+-]\\d{2}:\\d{2})?$/.test(${v}))return false`
2860
2976
  : `if(typeof ${v}==='string'&&!/^([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(\\.\\d+)?(Z|[+-]\\d{2}:\\d{2})?$/.test(${v}))return false`,
@@ -2867,15 +2983,9 @@ const FORMAT_CODEGEN = {
2867
2983
  'uri-reference': (v, isStr) => isStr
2868
2984
  ? `if(/[\\s\\u0000-\\u001f\\u007f]/.test(${v}))return false`
2869
2985
  : `if(typeof ${v}==='string'&&/[\\s\\u0000-\\u001f\\u007f]/.test(${v}))return false`,
2870
- ipv4: (v, isStr) => isStr
2871
- ? `{const _p=${v}.split('.');if(_p.length!==4||!_p.every(function(n){var x=+n;return x>=0&&x<=255&&String(x)===n}))return false}`
2872
- : `if(typeof ${v}==='string'){const _p=${v}.split('.');if(_p.length!==4||!_p.every(function(n){var x=+n;return x>=0&&x<=255&&String(x)===n}))return false}`,
2873
- ipv6: (v, isStr) => isStr
2874
- ? `{const _s=${v};if(_s===''||!/^[0-9a-fA-F:]+$/.test(_s)||_s.split(':').length<3||_s.split(':').length>8)return false}`
2875
- : `if(typeof ${v}==='string'){const _s=${v};if(_s===''||!/^[0-9a-fA-F:]+$/.test(_s)||_s.split(':').length<3||_s.split(':').length>8)return false}`,
2876
- hostname: (v, isStr) => isStr
2877
- ? `if(!/^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(${v}))return false`
2878
- : `if(typeof ${v}==='string'&&!/^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(${v}))return false`,
2986
+ ipv4: _formats.ipv4Source,
2987
+ ipv6: _formats.ipv6Source,
2988
+ hostname: _formats.hostnameSource,
2879
2989
  }
2880
2990
 
2881
2991
  // Safe key escaping: use JSON.stringify to handle all special chars (newlines, null bytes, etc.)
@@ -3020,8 +3130,6 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
3020
3130
  if (typeof schema === 'object' && schema !== null) {
3021
3131
  const s = JSON.stringify(schema)
3022
3132
  if (s.includes('unevaluatedProperties') || s.includes('unevaluatedItems')) return null
3023
- // Bail on self-referencing schemas — error codegen doesn't support recursion
3024
- if (s.includes('"$ref":"#"')) return null
3025
3133
  }
3026
3134
  if (typeof schema === 'boolean') {
3027
3135
  return schema
@@ -3032,19 +3140,16 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
3032
3140
  if (!sharedCodegenGate(schema, schemaMap)) return null
3033
3141
  if (schema.patternProperties) {
3034
3142
  for (const [pat, sub] of Object.entries(schema.patternProperties)) {
3035
- if (typeof sub === 'boolean') return null
3036
3143
  if (/\\[pP]\{/.test(pat)) return null
3037
3144
  if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
3038
3145
  }
3039
3146
  }
3040
3147
  if (schema.dependentSchemas) {
3041
3148
  for (const sub of Object.values(schema.dependentSchemas)) {
3042
- if (typeof sub === 'boolean') return null
3043
3149
  if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
3044
3150
  }
3045
3151
  }
3046
3152
  if (schema.propertyNames) {
3047
- if (typeof schema.propertyNames === 'boolean') return null
3048
3153
  const pn = schema.propertyNames
3049
3154
  const supported = ['maxLength', 'minLength', 'pattern', 'const', 'enum']
3050
3155
  const keys = Object.keys(pn).filter(k => k !== '$schema')
@@ -3087,7 +3192,12 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
3087
3192
  : null }
3088
3193
  ctx.helperCode.push('const _cpLen=s=>{let n=0;for(const _ of s)n++;return n}')
3089
3194
  const lines = []
3090
- genCodeE(schema, 'd', '', lines, ctx, '#')
3195
+ try {
3196
+ genCodeE(schema, 'd', '', lines, ctx, '#')
3197
+ } catch (e) {
3198
+ if (e === DECLINE) return null
3199
+ throw e
3200
+ }
3091
3201
  // Inline branch-collapse helper only when the schema actually used a
3092
3202
  // oneOf/anyOf branch path (tracked via ctx.usesBranchCollapse). Keeps the
3093
3203
  // AOT output small for schemas that don't compose. Standalone modules can't
@@ -3098,16 +3208,28 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
3098
3208
  if (lines.length === 0) return (d) => ({ valid: true, errors: [] })
3099
3209
 
3100
3210
  const checkStr = lines.join('\n ')
3211
+ const defSetsE = ctx.defFns ? Array.from(ctx.defFns.values()) : []
3212
+ const needsGuardE = ctx.usesRecursion || defSetsE.length > 0
3213
+ const guardStrE = needsGuardE ? emitGuardState() : ''
3214
+ const helpersE = ctx.helperCode.length ? ctx.helperCode.join('\n ') + '\n ' : ''
3215
+
3101
3216
  let body
3102
3217
  if (ctx.usesRecursion) {
3103
- body = `const _e=[];\n ` +
3104
- (ctx.helperCode.length ? ctx.helperCode.join('\n ') + '\n ' : '') +
3105
- `function _validateE(d,_all,_e){\n ${checkStr}\n }\n _validateE(d,_all,_e);\n ` +
3218
+ // The abandoned first pass may have pushed errors, so the retry starts
3219
+ // from an empty list and the reported errors come from the guarded run
3220
+ // alone.
3221
+ body = `const _e=[];\n ` + guardStrE + helpersE +
3222
+ emitRootGuard('_validateE', 'd,_all,_e', '') +
3223
+ `function _validateE_b(d,_all,_e){\n ${checkStr}\n }\n ` +
3224
+ emitGuardedRun('_validateE(d,_all,_e)', '_e.length=0;', defSetsE) +
3225
+ `return{valid:_e.length===0,errors:_e}`
3226
+ } else if (defSetsE.length > 0) {
3227
+ body = `const _e=[];\n ` + guardStrE + helpersE +
3228
+ `function _bodyE(d,_all,_e){\n ${checkStr}\n }\n ` +
3229
+ emitGuardedRun('_bodyE(d,_all,_e)', '_e.length=0;', defSetsE) +
3106
3230
  `return{valid:_e.length===0,errors:_e}`
3107
3231
  } else {
3108
- body = `const _e=[];\n ` +
3109
- (ctx.helperCode.length ? ctx.helperCode.join('\n ') + '\n ' : '') +
3110
- checkStr +
3232
+ body = `const _e=[];\n ` + helpersE + checkStr +
3111
3233
  `\n return{valid:_e.length===0,errors:_e}`
3112
3234
  }
3113
3235
  try {
@@ -3145,6 +3267,10 @@ function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaP
3145
3267
  const subSp = branchSp + '/' + i
3146
3268
  if (typeof sub === 'object' && sub !== null) {
3147
3269
  genCodeE(sub, '_bv', pathExpr, subLines, ctx, subSp)
3270
+ } else if (sub === false) {
3271
+ // A false branch never matches; it reports the same error the
3272
+ // interpreter attaches to a false schema.
3273
+ subLines.push(`_e.push({keyword:'not',instancePath:${pathExpr||'""'},schemaPath:'${subSp}',params:{},message:'boolean schema is false'})`)
3148
3274
  }
3149
3275
  const title = (sub && typeof sub === 'object' && typeof sub.title === 'string') ? sub.title : ''
3150
3276
  titles.push(title)
@@ -3166,19 +3292,54 @@ function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaP
3166
3292
  // `_all` parameter: if falsy, return after first error.
3167
3293
  function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
3168
3294
  if (!schemaPrefix) schemaPrefix = '#'
3295
+ // A false schema reports one error at the value that reached it, with the
3296
+ // keyword the interpreter has always used for it, `not` (lib/plan-compiler.js
3297
+ // line 58; its `err()` discards the 'false schema' label). Other validators
3298
+ // name this keyword `false schema`; changing it here would change output
3299
+ // the interpreter path already produces, so both engines say `not`.
3300
+ if (schema === false) {
3301
+ lines.push(`;_e.push({keyword:'not',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}',params:{},message:'boolean schema is false'});if(!_all)return{valid:false,errors:_e}`)
3302
+ return
3303
+ }
3304
+ if (schema === true) return
3169
3305
  if (typeof schema !== 'object' || schema === null) return
3170
3306
  if (!ctx.regExpMap) {
3171
3307
  ctx.regExpMap = new Map();
3172
3308
  }
3173
3309
  // $ref — resolve local and cross-schema refs
3174
3310
  if (schema.$ref) {
3175
- // Self-reference "#" — no-op (permissive) to avoid infinite recursion
3176
- if (schema.$ref === '#') return
3311
+ // A self-reference cannot be expressed here yet: the root helper takes no
3312
+ // path argument, so errors raised through it would carry the path of the
3313
+ // root rather than of the value that failed. This used to fall through as
3314
+ // a permissive no-op, which accepted whatever the reference guarded.
3315
+ if (schema.$ref === '#') throw DECLINE
3177
3316
  const m = schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
3178
3317
  if (m && ctx.rootDefs && ctx.rootDefs[m[1]]) {
3318
+ const defName = m[1]
3319
+ if (!ctx.cyclicDefs) ctx.cyclicDefs = cyclicDefNames(ctx.rootDefs)
3320
+ if (ctx.cyclicDefs.has(defName)) {
3321
+ // Named helper for a def on a cycle; the path comes in as an
3322
+ // argument because every call site has its own.
3323
+ if (!ctx.defFns) ctx.defFns = new Map()
3324
+ let fnName = ctx.defFns.get(defName)
3325
+ if (!fnName) {
3326
+ fnName = '_defE' + ctx.defFns.size + '_' + defName.replace(/[^A-Za-z0-9_]/g, '_')
3327
+ ctx.defFns.set(defName, fnName)
3328
+ const bodyLines = []
3329
+ genCodeE(ctx.rootDefs[defName], 'd', '_p', bodyLines, ctx, '#/$defs/' + defName)
3330
+ ctx.helperCode.push(
3331
+ `const ${fnName}_s=new Set()\n function ${fnName}(d,_p,_all,_e){\n ` +
3332
+ `if(_sg){if(typeof d!=='object'||d===null)return ${fnName}_b(d,_p,_all,_e);if(${fnName}_s.has(d))return;${fnName}_s.add(d);try{return ${fnName}_b(d,_p,_all,_e)}finally{${fnName}_s.delete(d)}}\n ` +
3333
+ `if(++_sd>${CYCLE_DEPTH})throw _CYC\n const _r=${fnName}_b(d,_p,_all,_e)\n _sd--\n return _r\n }\n ` +
3334
+ `function ${fnName}_b(d,_p,_all,_e){${bodyLines.join('\n ')}}`,
3335
+ )
3336
+ }
3337
+ lines.push(`${fnName}(${v},${pathExpr || '""'},_all,_e);if(!_all&&_e.length)return{valid:false,errors:_e}`)
3338
+ return
3339
+ }
3179
3340
  if (ctx.refStack.has(schema.$ref)) return
3180
3341
  ctx.refStack.add(schema.$ref)
3181
- genCodeE(ctx.rootDefs[m[1]], v, pathExpr, lines, ctx, schemaPrefix)
3342
+ genCodeE(ctx.rootDefs[defName], v, pathExpr, lines, ctx, schemaPrefix)
3182
3343
  ctx.refStack.delete(schema.$ref)
3183
3344
  return
3184
3345
  }
@@ -3496,7 +3657,26 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
3496
3657
  const ki = ctx.varCounter++
3497
3658
  const known = Object.keys(schema.properties || {})
3498
3659
  const guard = known.length ? `const _ak${ki}=new Set([${known.map((k) => JSON.stringify(k)).join(',')}]);` : ''
3499
- const keep = known.length ? `if(!_ak${ki}.has(_k${ki})){` : `{`
3660
+ // A key matched by any patternProperties entry is not additional.
3661
+ const patTests = []
3662
+ for (const pat of Object.keys(schema.patternProperties || {})) {
3663
+ const pattern = JSON.stringify(pat)
3664
+ if (!ctx.regExpMap.has(pattern)) {
3665
+ const ri = ctx.varCounter++
3666
+ ctx.regExpMap.set(pattern, ri)
3667
+ if (patternIsSafe(pat)) {
3668
+ ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`)
3669
+ ctx.usesSafeRe = true
3670
+ } else {
3671
+ ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`)
3672
+ }
3673
+ }
3674
+ patTests.push(`_re${ctx.regExpMap.get(pattern)}.test(_k${ki})`)
3675
+ }
3676
+ const conds = []
3677
+ if (known.length) conds.push(`!_ak${ki}.has(_k${ki})`)
3678
+ if (patTests.length) conds.push(`!(${patTests.join('||')})`)
3679
+ const keep = conds.length ? `if(${conds.join('&&')}){` : `{`
3500
3680
  lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${guard}for(const _k${ki} in ${v}){${keep}`)
3501
3681
  const p = pathExpr ? `${pathExpr}+'/'+_k${ki}` : `'/'+_k${ki}`
3502
3682
  genCodeE(schema.additionalProperties, `${v}[_k${ki}]`, p, lines, ctx, schemaPrefix + '/additionalProperties')
@@ -3512,6 +3692,14 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
3512
3692
  }
3513
3693
  }
3514
3694
 
3695
+ // propertyNames: false reports every key, at the key's own path, the way the
3696
+ // interpreter does (plan-compiler: instancePath + '/' + key).
3697
+ if (schema.propertyNames === false) {
3698
+ const ki = ctx.varCounter++
3699
+ const p = pathExpr ? `${pathExpr}+'/'+_k${ki}` : `'/'+_k${ki}`
3700
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){_e.push({keyword:'not',instancePath:${p},schemaPath:'${schemaPrefix}/propertyNames',params:{},message:'boolean schema is false'});if(!_all)return{valid:false,errors:_e}}}`)
3701
+ }
3702
+
3515
3703
  // propertyNames
3516
3704
  if (schema.propertyNames && typeof schema.propertyNames === 'object') {
3517
3705
  const pn = schema.propertyNames
@@ -3550,7 +3738,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
3550
3738
  }
3551
3739
 
3552
3740
  // items — starts after prefixItems (Draft 2020-12 semantics)
3553
- if (schema.items) {
3741
+ if (schema.items !== undefined && schema.items !== true) {
3554
3742
  const startIdx = schema.prefixItems ? schema.prefixItems.length : 0
3555
3743
  const idx = `_j${ctx.varCounter}`
3556
3744
  const elem = `_ei${ctx.varCounter}`
@@ -3572,7 +3760,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
3572
3760
  }
3573
3761
 
3574
3762
  // contains
3575
- if (schema.contains) {
3763
+ if (schema.contains !== undefined) {
3576
3764
  const ci = ctx.varCounter++
3577
3765
  const subLines = []
3578
3766
  genCode(schema.contains, `_cv`, subLines, ctx)
@@ -3605,7 +3793,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
3605
3793
  }
3606
3794
 
3607
3795
  // not
3608
- if (schema.not) {
3796
+ if (schema.not !== undefined) {
3609
3797
  const subLines = []
3610
3798
  genCode(schema.not, '_nv', subLines, ctx)
3611
3799
  const nfn = subLines.length === 0 ? `function(_nv){return true}` : `function(_nv){${subLines.join(';')};return true}`
@@ -3614,7 +3802,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
3614
3802
  }
3615
3803
 
3616
3804
  // if/then/else
3617
- if (schema.if) {
3805
+ if (schema.if !== undefined) {
3618
3806
  const ifLines = []
3619
3807
  genCode(schema.if, '_iv', ifLines, ctx)
3620
3808
  const fi = ctx.varCounter++
@@ -3622,13 +3810,13 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
3622
3810
  ? `function(_iv){return true}`
3623
3811
  : `function(_iv){${ifLines.join(';')};return true}`
3624
3812
  lines.push(`{const _if${fi}=${ifFn}`)
3625
- if (schema.then) {
3813
+ if (schema.then !== undefined) {
3626
3814
  lines.push(`if(_if${fi}(${v})){`)
3627
3815
  genCodeE(schema.then, v, pathExpr, lines, ctx, schemaPrefix+'/then')
3628
3816
  lines.push(`}`)
3629
3817
  }
3630
- if (schema.else) {
3631
- lines.push(`${schema.then ? 'else' : `if(!_if${fi}(${v}))`}{`)
3818
+ if (schema.else !== undefined) {
3819
+ lines.push(`${schema.then !== undefined ? 'else' : `if(!_if${fi}(${v}))`}{`)
3632
3820
  genCodeE(schema.else, v, pathExpr, lines, ctx, schemaPrefix+'/else')
3633
3821
  lines.push(`}`)
3634
3822
  }
@@ -3647,6 +3835,10 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats) {
3647
3835
  if (s.includes('unevaluatedProperties') || s.includes('unevaluatedItems')) return null
3648
3836
  // Bail on self-referencing schemas — combined codegen doesn't support recursion
3649
3837
  if (s.includes('"$ref":"#"')) return null
3838
+ // Defs on a $ref cycle compile as named functions in the boolean and
3839
+ // errors generators; this generator has no preamble for them, so it
3840
+ // declines and the router uses the errors generator.
3841
+ if (cyclicDefNames(schema.$defs || schema.definitions).size) return null
3650
3842
  // Bail on additionalProperties as a schema — combined path doesn't emit the
3651
3843
  // per-key sub-schema loop yet. The jsFn + errFn fallback handles it correctly.
3652
3844
  if (hasAdditionalPropertiesSchema(schema)) return null
@@ -3665,19 +3857,16 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats) {
3665
3857
  if (!sharedCodegenGate(schema, schemaMap)) return null
3666
3858
  if (schema.patternProperties) {
3667
3859
  for (const [pat, sub] of Object.entries(schema.patternProperties)) {
3668
- if (typeof sub === 'boolean') return null
3669
3860
  if (/\\[pP]\{/.test(pat)) return null
3670
3861
  if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
3671
3862
  }
3672
3863
  }
3673
3864
  if (schema.dependentSchemas) {
3674
3865
  for (const sub of Object.values(schema.dependentSchemas)) {
3675
- if (typeof sub === 'boolean') return null
3676
3866
  if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
3677
3867
  }
3678
3868
  }
3679
3869
  if (schema.propertyNames) {
3680
- if (typeof schema.propertyNames === 'boolean') return null
3681
3870
  const pn = schema.propertyNames
3682
3871
  const supported = ['maxLength', 'minLength', 'pattern', 'const', 'enum']
3683
3872
  const keys = Object.keys(pn).filter(k => k !== '$schema')
@@ -3747,6 +3936,13 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats) {
3747
3936
  // If type check fails → push error, skip property checks (they'd crash).
3748
3937
  function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
3749
3938
  if (!schemaPrefix) schemaPrefix = '#'
3939
+ if (schema === false) {
3940
+ // Leading semicolon: the previous generated line may end in an
3941
+ // expression, and a line starting with `(` would be parsed as a call.
3942
+ lines.push(`;(_e||(_e=[])).push({keyword:'not',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}',params:{},message:'boolean schema is false'})`)
3943
+ return
3944
+ }
3945
+ if (schema === true) return
3750
3946
  if (typeof schema !== 'object' || schema === null) return
3751
3947
 
3752
3948
  // $ref — resolve local, anchor, and cross-schema refs
@@ -4181,6 +4377,12 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4181
4377
  }
4182
4378
  }
4183
4379
 
4380
+ if (schema.propertyNames === false) {
4381
+ const ki = ctx.varCounter++
4382
+ const p = pathExpr ? `${pathExpr}+'/'+_k${ki}` : `'/'+_k${ki}`
4383
+ lines.push(`;if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){(_e||(_e=[])).push({keyword:'not',instancePath:${p},schemaPath:'${schemaPrefix}/propertyNames',params:{},message:'boolean schema is false'})}}`)
4384
+ }
4385
+
4184
4386
  // propertyNames — skip if already merged into patternProperties loop
4185
4387
  if (schema.propertyNames && typeof schema.propertyNames === 'object' && !ctx._ppHandledPropertyNamesC) {
4186
4388
  const pn = schema.propertyNames
@@ -4211,7 +4413,7 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4211
4413
  }
4212
4414
 
4213
4415
  // items
4214
- if (schema.items) {
4416
+ if (schema.items !== undefined && schema.items !== true) {
4215
4417
  const startIdx = schema.prefixItems ? schema.prefixItems.length : 0
4216
4418
  const idx = `_j${ctx.varCounter}`, elem = `_ei${ctx.varCounter}`
4217
4419
  ctx.varCounter++
@@ -4232,7 +4434,7 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4232
4434
  }
4233
4435
 
4234
4436
  // contains
4235
- if (schema.contains) {
4437
+ if (schema.contains !== undefined) {
4236
4438
  const ci = ctx.varCounter++
4237
4439
  const subLines = []
4238
4440
  genCode(schema.contains, `_cv`, subLines, ctx)
@@ -4263,7 +4465,7 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4263
4465
  }
4264
4466
 
4265
4467
  // not
4266
- if (schema.not) {
4468
+ if (schema.not !== undefined) {
4267
4469
  const sl = []; genCode(schema.not, '_nv', sl, ctx)
4268
4470
  const nfn = sl.length === 0 ? `function(_nv){return true}` : `function(_nv){${sl.join(';')};return true}`
4269
4471
  const fi = ctx.varCounter++
@@ -4271,13 +4473,13 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4271
4473
  }
4272
4474
 
4273
4475
  // if/then/else
4274
- if (schema.if) {
4476
+ if (schema.if !== undefined) {
4275
4477
  const sl = []; genCode(schema.if, '_iv', sl, ctx)
4276
4478
  const fi = ctx.varCounter++
4277
4479
  const ifFn = sl.length === 0 ? `function(_iv){return true}` : `function(_iv){${sl.join(';')};return true}`
4278
4480
  lines.push(`{const _if${fi}=${ifFn}`)
4279
- if (schema.then) { lines.push(`if(_if${fi}(${v})){`); genCodeC(schema.then, v, pathExpr, lines, ctx, schemaPrefix+'/then'); lines.push(`}`) }
4280
- if (schema.else) { lines.push(`${schema.then ? 'else' : `if(!_if${fi}(${v}))`}{`); genCodeC(schema.else, v, pathExpr, lines, ctx, schemaPrefix+'/else'); lines.push(`}`) }
4481
+ if (schema.then !== undefined) { lines.push(`if(_if${fi}(${v})){`); genCodeC(schema.then, v, pathExpr, lines, ctx, schemaPrefix+'/then'); lines.push(`}`) }
4482
+ if (schema.else !== undefined) { lines.push(`${schema.then !== undefined ? 'else' : `if(!_if${fi}(${v}))`}{`); genCodeC(schema.else, v, pathExpr, lines, ctx, schemaPrefix+'/else'); lines.push(`}`) }
4281
4483
  lines.push(`}`)
4282
4484
  }
4283
4485
 
@@ -4362,7 +4564,7 @@ function _collectEval(schema, result, defs, schemaMap, refStack, isRoot) {
4362
4564
 
4363
4565
  // contains: marks matching items as evaluated (not ALL items)
4364
4566
  // Always set dynamic since which items match depends on the data
4365
- if (schema.contains) {
4567
+ if (schema.contains !== undefined) {
4366
4568
  result.dynamic = true
4367
4569
  }
4368
4570