ata-validator 0.7.3 → 0.9.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.
@@ -39,7 +39,11 @@ function compileToJS(schema, defs, schemaMap) {
39
39
  if (typeof schema !== 'object' || schema === null) return null
40
40
 
41
41
  // Bail if schema has edge cases that JS fast path gets wrong
42
- if (!defs && !codegenSafe(schema, schemaMap)) return null
42
+ // Exception: $dynamicRef/$anchor are handled by the interpretive path even though codegen can't
43
+ if (!defs && !codegenSafe(schema, schemaMap)) {
44
+ const str = JSON.stringify(schema)
45
+ if (!str.includes('"$dynamicRef"') && !str.includes('"$dynamicAnchor"') && !str.includes('"$anchor"')) return null
46
+ }
43
47
 
44
48
  // Collect $defs early so sub-schemas can resolve $ref
45
49
  const rootDefs = defs || collectDefs(schema)
@@ -60,6 +64,23 @@ function compileToJS(schema, defs, schemaMap) {
60
64
  checks.push(refFn)
61
65
  }
62
66
 
67
+ // $dynamicRef — resolve via anchor defs or JSON pointer
68
+ if (schema.$dynamicRef) {
69
+ const ref = schema.$dynamicRef
70
+ const anchorName = ref.startsWith('#') ? ref : '#' + ref
71
+ if (rootDefs && rootDefs[anchorName]) {
72
+ const entry = rootDefs[anchorName]
73
+ checks.push((d) => { const fn = entry.fn; return fn ? fn(d) : true })
74
+ } else {
75
+ // JSON pointer: "#/$defs/foo" or "#/definitions/foo"
76
+ const m = ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
77
+ if (m && rootDefs && rootDefs[m[1]]) {
78
+ const entry = rootDefs[m[1]]
79
+ checks.push((d) => { const fn = entry.fn; return fn ? fn(d) : true })
80
+ }
81
+ }
82
+ }
83
+
63
84
  // type
64
85
  if (schema.type) {
65
86
  const types = Array.isArray(schema.type) ? schema.type : [schema.type]
@@ -395,12 +416,75 @@ function collectDefs(schema) {
395
416
  },
396
417
  raw: def
397
418
  }
419
+ // Register anchors
420
+ if (def && typeof def === 'object') {
421
+ if (def.$anchor) {
422
+ const anchorDef = def
423
+ let anchorCached = undefined
424
+ defs['#' + def.$anchor] = {
425
+ get fn() {
426
+ if (anchorCached === undefined) {
427
+ anchorCached = null
428
+ anchorCached = compileToJS(anchorDef, defs)
429
+ }
430
+ return anchorCached || (() => true)
431
+ },
432
+ raw: anchorDef
433
+ }
434
+ }
435
+ if (def.$dynamicAnchor) {
436
+ const daDef = def
437
+ let daCached = undefined
438
+ defs['#' + def.$dynamicAnchor] = {
439
+ get fn() {
440
+ if (daCached === undefined) {
441
+ daCached = null
442
+ daCached = compileToJS(daDef, defs)
443
+ }
444
+ return daCached || (() => true)
445
+ },
446
+ raw: daDef
447
+ }
448
+ }
449
+ }
450
+ }
451
+ }
452
+ // Register root-level $anchor/$dynamicAnchor (self-referencing schemas)
453
+ if (schema.$anchor && !defs['#' + schema.$anchor]) {
454
+ const rootAnchorSchema = schema
455
+ let rootACached = undefined
456
+ defs['#' + schema.$anchor] = {
457
+ get fn() {
458
+ if (rootACached === undefined) {
459
+ rootACached = null
460
+ rootACached = compileToJS(rootAnchorSchema, defs)
461
+ }
462
+ return rootACached || (() => true)
463
+ },
464
+ raw: rootAnchorSchema
465
+ }
466
+ }
467
+ if (schema.$dynamicAnchor && !defs['#' + schema.$dynamicAnchor]) {
468
+ const rootDASchema = schema
469
+ let rootDACached = undefined
470
+ defs['#' + schema.$dynamicAnchor] = {
471
+ get fn() {
472
+ if (rootDACached === undefined) {
473
+ rootDACached = null
474
+ rootDACached = compileToJS(rootDASchema, defs)
475
+ }
476
+ return rootDACached || (() => true)
477
+ },
478
+ raw: rootDASchema
398
479
  }
399
480
  }
400
481
  return defs
401
482
  }
402
483
 
403
484
  function resolveRef(ref, defs, schemaMap) {
485
+ // Self-reference: "#" — treat as permissive to avoid infinite recursion
486
+ if (ref === '#') return () => true
487
+
404
488
  // 1. Local ref
405
489
  if (defs) {
406
490
  const m = ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
@@ -409,13 +493,28 @@ function resolveRef(ref, defs, schemaMap) {
409
493
  const entry = defs[name]
410
494
  if (entry) return (d) => { const fn = entry.fn; return fn ? fn(d) : true }
411
495
  }
496
+ // Anchor ref: "#foo"
497
+ if (ref.startsWith('#') && !ref.startsWith('#/')) {
498
+ const entry = defs[ref]
499
+ if (entry) return (d) => { const fn = entry.fn; return fn ? fn(d) : true }
500
+ }
412
501
  }
413
- // 2. Cross-schema ref
502
+ // 2. Cross-schema ref (exact match)
414
503
  if (schemaMap && schemaMap.has(ref)) {
415
504
  const resolved = schemaMap.get(ref)
416
505
  const fn = compileToJS(resolved, null, schemaMap)
417
506
  return fn || (() => true)
418
507
  }
508
+ // 3. Cross-schema ref (relative URI resolution)
509
+ if (schemaMap && !ref.includes('://') && !ref.startsWith('#')) {
510
+ for (const [id] of schemaMap) {
511
+ if (id.endsWith('/' + ref)) {
512
+ const resolved = schemaMap.get(id)
513
+ const fn = compileToJS(resolved, null, schemaMap)
514
+ return fn || (() => true)
515
+ }
516
+ }
517
+ }
419
518
  return null
420
519
  }
421
520
 
@@ -444,7 +543,7 @@ const TYPE_CHECKS = {
444
543
 
445
544
  const FORMAT_CHECKS = {
446
545
  email: (s) => { const at = s.indexOf('@'); return at > 0 && at < s.length - 1 && s.indexOf('.', at) > at + 1 },
447
- date: (s) => s.length === 10 && /^\d{4}-\d{2}-\d{2}$/.test(s),
546
+ 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 },
448
547
  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),
449
548
  ipv4: (s) => { const p = s.split('.'); return p.length === 4 && p.every(n => { const v = +n; return v >= 0 && v <= 255 && String(v) === n }) },
450
549
  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),
@@ -454,12 +553,49 @@ const FORMAT_CHECKS = {
454
553
  const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'toString', 'valueOf',
455
554
  'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', 'toLocaleString'])
456
555
 
556
+ // Check if all $dynamicRef in a target schema can be resolved via the calling schema's anchors.
557
+ function canResolveDynamicRefs(target, callingSchema, schemaMap) {
558
+ // Collect anchors from calling schema
559
+ const anchors = new Set()
560
+ if (callingSchema.$dynamicAnchor) anchors.add(callingSchema.$dynamicAnchor)
561
+ const defs = callingSchema.$defs || callingSchema.definitions
562
+ if (defs) {
563
+ for (const def of Object.values(defs)) {
564
+ if (def && typeof def === 'object' && def.$dynamicAnchor) anchors.add(def.$dynamicAnchor)
565
+ }
566
+ }
567
+ // Also collect from schemaMap
568
+ if (schemaMap) {
569
+ for (const ext of schemaMap.values()) {
570
+ if (ext && typeof ext === 'object' && ext.$dynamicAnchor) anchors.add(ext.$dynamicAnchor)
571
+ }
572
+ }
573
+ // Find all $dynamicRef in target
574
+ const refs = []
575
+ const findDynRefs = (s) => {
576
+ if (typeof s !== 'object' || s === null) return
577
+ if (s.$dynamicRef) {
578
+ const name = s.$dynamicRef.startsWith('#') ? s.$dynamicRef.slice(1) : s.$dynamicRef
579
+ refs.push(name)
580
+ }
581
+ for (const v of Object.values(s)) {
582
+ if (Array.isArray(v)) v.forEach(findDynRefs)
583
+ else if (typeof v === 'object' && v !== null) findDynRefs(v)
584
+ }
585
+ }
586
+ findDynRefs(target)
587
+ return refs.every(r => anchors.has(r))
588
+ }
589
+
457
590
  // Recursively check if a schema can be safely compiled to JS codegen.
458
591
  // Returns false if any sub-schema contains features codegen gets wrong.
459
592
  function codegenSafe(schema, schemaMap) {
460
593
  if (typeof schema === 'boolean') return true
461
594
  if (typeof schema !== 'object' || schema === null) return true
462
595
 
596
+ // Only bail on $dynamicRef if it can't be resolved at compile time
597
+ if (schema.$dynamicRef && !schema.$dynamicRef.startsWith('#')) return false
598
+
463
599
  // Boolean sub-schemas anywhere cause bail — codegen doesn't handle schema=false correctly
464
600
  if (schema.items === false) return false
465
601
  if (schema.items === true && !schema.unevaluatedItems) return false
@@ -490,12 +626,37 @@ function codegenSafe(schema, schemaMap) {
490
626
 
491
627
  // $ref — allow local refs (#/$defs/Name) and non-local refs if in schemaMap
492
628
  if (schema.$ref) {
629
+ // Self-reference "#" — treated as permissive (no-op) to avoid infinite recursion
630
+ if (schema.$ref === '#') return true
493
631
  const isLocal = /^#\/(?:\$defs|definitions)\/[^/]+$/.test(schema.$ref)
494
- const isResolvable = !isLocal && schemaMap && schemaMap.has(schema.$ref)
495
- if (!isLocal && !isResolvable) return false
632
+ let isResolvable = !isLocal && schemaMap && schemaMap.has(schema.$ref)
633
+ // Relative URI resolution: check if any schemaMap key ends with "/" + ref
634
+ let resolvedTarget = null
635
+ if (!isLocal && !isResolvable && schemaMap && !schema.$ref.includes('://') && !schema.$ref.startsWith('#')) {
636
+ for (const [id] of schemaMap) {
637
+ if (id.endsWith('/' + schema.$ref)) { isResolvable = true; resolvedTarget = schemaMap.get(id); break }
638
+ }
639
+ }
640
+ // Anchor-style ref: #name (not #/path, not bare #) — resolvable at compile time via anchors map
641
+ const isAnchorRef = !isLocal && !isResolvable && schema.$ref.length > 1 && schema.$ref.startsWith('#') && !schema.$ref.startsWith('#/')
642
+ if (!isLocal && !isResolvable && !isAnchorRef) return false
643
+ // If the resolved target contains $dynamicRef, allow codegen only when:
644
+ // 1. All $dynamicRefs can be resolved via the current schema's anchors
645
+ // 2. The resolved target itself is simple enough for codegen (no additionalProperties: false, etc.)
646
+ if (!resolvedTarget && isResolvable) resolvedTarget = schemaMap.get(schema.$ref)
647
+ if (resolvedTarget && JSON.stringify(resolvedTarget).includes('"$dynamicRef"')) {
648
+ const canResolve = canResolveDynamicRefs(resolvedTarget, schema, schemaMap)
649
+ // Also verify the resolved target doesn't have complex features that codegen can't inline
650
+ const targetSimple = canResolve && resolvedTarget.additionalProperties === undefined &&
651
+ !resolvedTarget.patternProperties && !resolvedTarget.dependentSchemas &&
652
+ !resolvedTarget.propertyNames
653
+ if (!targetSimple && schema.unevaluatedProperties === undefined && schema.unevaluatedItems === undefined) return false
654
+ }
496
655
  // In Draft 2020-12, $ref with siblings is allowed. Only bail if no unevaluated* keyword
497
656
  // (unevaluated schemas need $ref + siblings to work properly)
498
- const siblings = Object.keys(schema).filter(k => k !== '$ref' && k !== '$defs' && k !== 'definitions' && k !== '$schema' && k !== '$id')
657
+ // Schema-organization keywords ($dynamicAnchor, $anchor) are not validation siblings
658
+ const SCHEMA_ORG_KEYS = new Set(['$ref', '$defs', 'definitions', '$schema', '$id', '$dynamicAnchor', '$anchor'])
659
+ const siblings = Object.keys(schema).filter(k => !SCHEMA_ORG_KEYS.has(k))
499
660
  if (siblings.length > 0 && schema.unevaluatedProperties === undefined && schema.unevaluatedItems === undefined) return false
500
661
  }
501
662
 
@@ -590,7 +751,31 @@ function compileToJSCodegen(schema, schemaMap) {
590
751
  if (keys.some(k => !supported.includes(k))) return null
591
752
  }
592
753
 
593
- const ctx = { varCounter: 0, helpers: [], helperCode: [], closureVars: [], closureVals: [], rootDefs, refStack: new Set(), schemaMap: schemaMap || null }
754
+ // Build anchors map for $ref/#anchor and $dynamicRef resolution
755
+ const anchors = {}
756
+ // Root schema's own $dynamicAnchor / $anchor
757
+ if (schema.$dynamicAnchor) anchors['#' + schema.$dynamicAnchor] = schema
758
+ if (schema.$anchor) anchors['#' + schema.$anchor] = schema
759
+ // Anchors from $defs
760
+ if (rootDefs) {
761
+ for (const def of Object.values(rootDefs)) {
762
+ if (def && typeof def === 'object') {
763
+ if (def.$dynamicAnchor) anchors['#' + def.$dynamicAnchor] = def
764
+ if (def.$anchor) anchors['#' + def.$anchor] = def
765
+ }
766
+ }
767
+ }
768
+ // Anchors from external schemas in schemaMap
769
+ if (schemaMap) {
770
+ for (const ext of schemaMap.values()) {
771
+ if (ext && typeof ext === 'object') {
772
+ if (ext.$dynamicAnchor && !anchors['#' + ext.$dynamicAnchor]) anchors['#' + ext.$dynamicAnchor] = ext
773
+ if (ext.$anchor && !anchors['#' + ext.$anchor]) anchors['#' + ext.$anchor] = ext
774
+ }
775
+ }
776
+ }
777
+
778
+ const ctx = { varCounter: 0, helpers: [], helperCode: [], closureVars: [], closureVals: [], rootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors, rootSchema: schema }
594
779
  const lines = []
595
780
  genCode(schema, 'd', lines, ctx)
596
781
 
@@ -616,7 +801,16 @@ function compileToJSCodegen(schema, schemaMap) {
616
801
  }
617
802
  }
618
803
 
619
- const body = checkStr + '\n return true'
804
+ let body, hybridBody
805
+ if (ctx.usesRecursion) {
806
+ // Self-recursive: wrap in named function
807
+ body = `function _validate(d){\n ${checkStr}\n return true\n }\n return _validate(d)`
808
+ // Hybrid: keep _validate as boolean, wrap only the outer call
809
+ hybridBody = `function _validate(d){\n ${checkStr}\n return true\n }\n return _validate(d)?R:E(d)`
810
+ } else {
811
+ body = checkStr + '\n return true'
812
+ hybridBody = replaceTopLevel(checkStr + '\n return R')
813
+ }
620
814
 
621
815
  try {
622
816
  let boolFn
@@ -628,7 +822,6 @@ function compileToJSCodegen(schema, schemaMap) {
628
822
  }
629
823
 
630
824
  // Build hybrid: same body, return R instead of true, return E(d) instead of false.
631
- const hybridBody = replaceTopLevel(checkStr + '\n return R')
632
825
  try {
633
826
  const hybridFactory = new Function(...closureNames, 'R', 'E', `return function(d){${hybridBody}}`)
634
827
  boolFn._hybridFactory = (R, E) => hybridFactory(...closureValues, R, E)
@@ -679,6 +872,84 @@ function replaceTopLevel(code) {
679
872
  return result
680
873
  }
681
874
 
875
+ // Returns true if a property sub-schema will generate 2+ lines that each access v,
876
+ // meaning a local variable hoist is worthwhile.
877
+ function needsLocal(schema) {
878
+ if (typeof schema !== 'object' || schema === null) return false
879
+ // If it has $ref, allOf, anyOf etc., genCode handles it — don't hoist
880
+ if (schema.$ref || schema.allOf || schema.anyOf || schema.oneOf || schema.if) return false
881
+ if (schema.properties || schema.items || schema.prefixItems) return false
882
+ const types = schema.type ? (Array.isArray(schema.type) ? schema.type : [schema.type]) : null
883
+ if (!types || types.length !== 1) return false
884
+ const t = types[0]
885
+ let checkCount = 1 // type check itself
886
+ if (t === 'string') {
887
+ if (schema.minLength !== undefined) checkCount++
888
+ if (schema.maxLength !== undefined) checkCount++
889
+ if (schema.pattern) checkCount++
890
+ if (schema.format) checkCount++
891
+ } else if (t === 'integer' || t === 'number') {
892
+ if (schema.minimum !== undefined) checkCount++
893
+ if (schema.maximum !== undefined) checkCount++
894
+ if (schema.exclusiveMinimum !== undefined) checkCount++
895
+ if (schema.exclusiveMaximum !== undefined) checkCount++
896
+ if (schema.multipleOf !== undefined) checkCount++
897
+ }
898
+ return checkCount >= 2
899
+ }
900
+
901
+ // Try to generate a single combined check for simple leaf schemas.
902
+ // Returns a string like "{const _v=d["x"];if(typeof _v!=='string'||_v.length<1||_v.length>100)return false}"
903
+ // or null if the schema is too complex.
904
+ function tryGenCombined(schema, access, ctx) {
905
+ if (typeof schema !== 'object' || schema === null) return null
906
+ // Only handle simple leaf schemas with a single type and basic constraints
907
+ if (schema.$ref || schema.allOf || schema.anyOf || schema.oneOf || schema.if) return null
908
+ if (schema.properties || schema.items || schema.prefixItems || schema.patternProperties) return null
909
+ if (schema.enum || schema.const !== undefined) return null
910
+ if (schema.not || schema.dependentRequired || schema.dependentSchemas) return null
911
+ const types = schema.type ? (Array.isArray(schema.type) ? schema.type : [schema.type]) : null
912
+ if (!types || types.length !== 1) return null
913
+ const t = types[0]
914
+
915
+ if (t === 'string') {
916
+ const conds = [`typeof _v!=='string'`]
917
+ if (schema.minLength !== undefined) conds.push(`_v.length<${schema.minLength}`)
918
+ if (schema.maxLength !== undefined) conds.push(`_v.length>${schema.maxLength}`)
919
+ if (conds.length < 2 && !schema.pattern && !schema.format) return null // not worth combining
920
+ // pattern and format need separate statements, fall back if present
921
+ if (schema.pattern || schema.format) return null
922
+ const vi = ctx.varCounter++
923
+ return `{const _v=${access};if(${conds.join('||')})return false}`
924
+ }
925
+
926
+ if (t === 'integer') {
927
+ const conds = [`!Number.isInteger(_v)`]
928
+ if (schema.minimum !== undefined) conds.push(`_v<${schema.minimum}`)
929
+ if (schema.maximum !== undefined) conds.push(`_v>${schema.maximum}`)
930
+ if (schema.exclusiveMinimum !== undefined) conds.push(`_v<=${schema.exclusiveMinimum}`)
931
+ if (schema.exclusiveMaximum !== undefined) conds.push(`_v>=${schema.exclusiveMaximum}`)
932
+ if (schema.multipleOf !== undefined) conds.push(`_v%${schema.multipleOf}!==0`)
933
+ if (conds.length < 2) return null
934
+ const vi = ctx.varCounter++
935
+ return `{const _v=${access};if(${conds.join('||')})return false}`
936
+ }
937
+
938
+ if (t === 'number') {
939
+ const conds = [`typeof _v!=='number'||!isFinite(_v)`]
940
+ if (schema.minimum !== undefined) conds.push(`_v<${schema.minimum}`)
941
+ if (schema.maximum !== undefined) conds.push(`_v>${schema.maximum}`)
942
+ if (schema.exclusiveMinimum !== undefined) conds.push(`_v<=${schema.exclusiveMinimum}`)
943
+ if (schema.exclusiveMaximum !== undefined) conds.push(`_v>=${schema.exclusiveMaximum}`)
944
+ if (schema.multipleOf !== undefined) conds.push(`_v%${schema.multipleOf}!==0`)
945
+ if (conds.length < 2) return null
946
+ const vi = ctx.varCounter++
947
+ return `{const _v=${access};if(${conds.join('||')})return false}`
948
+ }
949
+
950
+ return null
951
+ }
952
+
682
953
  // knownType: if parent already verified the type, skip redundant guards.
683
954
  // 'object' = we know v is a non-null non-array object
684
955
  // 'array' = we know v is an array
@@ -691,8 +962,14 @@ function genCode(schema, v, lines, ctx, knownType) {
691
962
  // Only when THIS schema has unevaluated keywords directly (not via $ref target)
692
963
  const hasSiblings = schema.$ref && (schema.unevaluatedProperties !== undefined || schema.unevaluatedItems !== undefined)
693
964
  if (schema.$ref) {
965
+ // Self-reference "#" — recursive call to root validator
966
+ if (schema.$ref === '#') {
967
+ ctx.usesRecursion = true
968
+ lines.push(`if(!_validate(${v}))return false`)
969
+ if (!hasSiblings) return
970
+ }
694
971
  // 1. Local ref
695
- const m = schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
972
+ const m = schema.$ref !== '#' && schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
696
973
  if (m && ctx.rootDefs && ctx.rootDefs[m[1]]) {
697
974
  if (ctx.refStack.has(schema.$ref)) { if (!hasSiblings) return }
698
975
  else {
@@ -701,13 +978,36 @@ function genCode(schema, v, lines, ctx, knownType) {
701
978
  ctx.refStack.delete(schema.$ref)
702
979
  if (!hasSiblings) return
703
980
  }
704
- } else if (ctx.schemaMap && ctx.schemaMap.has(schema.$ref)) {
705
- // 2. Cross-schema ref
706
- if (ctx.refStack.has(schema.$ref)) { if (!hasSiblings) return }
707
- else {
708
- ctx.refStack.add(schema.$ref)
709
- genCode(ctx.schemaMap.get(schema.$ref), v, lines, ctx, knownType)
710
- ctx.refStack.delete(schema.$ref)
981
+ } else if (schema.$ref !== '#' && !m && schema.$ref.startsWith('#') && !schema.$ref.startsWith('#/')) {
982
+ // Anchor ref: "#foo" — resolve via rootDefs or anchors map
983
+ const entry = ctx.rootDefs && ctx.rootDefs[schema.$ref]
984
+ const anchorTarget = entry && entry.raw ? entry.raw : (ctx.anchors && ctx.anchors[schema.$ref])
985
+ if (anchorTarget) {
986
+ if (ctx.refStack.has(schema.$ref)) { if (!hasSiblings) return }
987
+ else {
988
+ ctx.refStack.add(schema.$ref)
989
+ genCode(anchorTarget, v, lines, ctx, knownType)
990
+ ctx.refStack.delete(schema.$ref)
991
+ if (!hasSiblings) return
992
+ }
993
+ }
994
+ } else if (schema.$ref !== '#' && ctx.schemaMap) {
995
+ // 2. Cross-schema ref (exact match or relative URI)
996
+ let resolved = ctx.schemaMap.get(schema.$ref)
997
+ if (!resolved && !schema.$ref.includes('://') && !schema.$ref.startsWith('#')) {
998
+ for (const [id, s] of ctx.schemaMap) {
999
+ if (id.endsWith('/' + schema.$ref)) { resolved = s; break }
1000
+ }
1001
+ }
1002
+ if (resolved) {
1003
+ if (ctx.refStack.has(schema.$ref)) { if (!hasSiblings) return }
1004
+ else {
1005
+ ctx.refStack.add(schema.$ref)
1006
+ genCode(resolved, v, lines, ctx, knownType)
1007
+ ctx.refStack.delete(schema.$ref)
1008
+ if (!hasSiblings) return
1009
+ }
1010
+ } else {
711
1011
  if (!hasSiblings) return
712
1012
  }
713
1013
  } else {
@@ -715,25 +1015,58 @@ function genCode(schema, v, lines, ctx, knownType) {
715
1015
  }
716
1016
  }
717
1017
 
1018
+ // $dynamicRef — resolve via anchors map
1019
+ if (schema.$dynamicRef) {
1020
+ const anchorKey = schema.$dynamicRef.startsWith('#') ? schema.$dynamicRef : '#' + schema.$dynamicRef
1021
+ if (ctx.anchors && ctx.anchors[anchorKey]) {
1022
+ const target = ctx.anchors[anchorKey]
1023
+ if (target === ctx.rootSchema) {
1024
+ // Self-recursive: generate _validate(v) call
1025
+ ctx.usesRecursion = true
1026
+ lines.push(`if(!_validate(${v}))return false`)
1027
+ } else {
1028
+ // Different schema: inline the target validation
1029
+ const refKey = '$dynamicRef:' + anchorKey
1030
+ if (!ctx.refStack.has(refKey)) {
1031
+ ctx.refStack.add(refKey)
1032
+ genCode(target, v, lines, ctx, knownType)
1033
+ ctx.refStack.delete(refKey)
1034
+ }
1035
+ }
1036
+ }
1037
+ }
1038
+
718
1039
  // Determine the single known type after this schema's type check
719
1040
  const types = schema.type ? (Array.isArray(schema.type) ? schema.type : [schema.type]) : null
720
1041
  let effectiveType = knownType
721
1042
  if (types) {
722
1043
  if (!knownType) {
723
- // Emit the type check
724
- const conds = types.map(t => {
725
- switch (t) {
726
- case 'object': return `(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`
727
- case 'array': return `Array.isArray(${v})`
728
- case 'string': return `typeof ${v}==='string'`
729
- case 'number': return `(typeof ${v}==='number'&&isFinite(${v}))`
730
- case 'integer': return `Number.isInteger(${v})`
731
- case 'boolean': return `(${v}===true||${v}===false)`
732
- case 'null': return `${v}===null`
733
- default: return 'true'
1044
+ // Emit the type check — use direct negation for single types (avoids !() wrapper)
1045
+ if (types.length === 1) {
1046
+ switch (types[0]) {
1047
+ case 'object': lines.push(`if(typeof ${v}!=='object'||${v}===null||Array.isArray(${v}))return false`); break
1048
+ case 'array': lines.push(`if(!Array.isArray(${v}))return false`); break
1049
+ case 'string': lines.push(`if(typeof ${v}!=='string')return false`); break
1050
+ case 'number': lines.push(`if(typeof ${v}!=='number'||!isFinite(${v}))return false`); break
1051
+ case 'integer': lines.push(`if(!Number.isInteger(${v}))return false`); break
1052
+ case 'boolean': lines.push(`if(typeof ${v}!=='boolean')return false`); break
1053
+ case 'null': lines.push(`if(${v}!==null)return false`); break
734
1054
  }
735
- })
736
- lines.push(`if(!(${conds.join('||')}))return false`)
1055
+ } else {
1056
+ const conds = types.map(t => {
1057
+ switch (t) {
1058
+ case 'object': return `(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`
1059
+ case 'array': return `Array.isArray(${v})`
1060
+ case 'string': return `typeof ${v}==='string'`
1061
+ case 'number': return `(typeof ${v}==='number'&&isFinite(${v}))`
1062
+ case 'integer': return `Number.isInteger(${v})`
1063
+ case 'boolean': return `typeof ${v}==='boolean'`
1064
+ case 'null': return `${v}===null`
1065
+ default: return 'true'
1066
+ }
1067
+ })
1068
+ lines.push(`if(!(${conds.join('||')}))return false`)
1069
+ }
737
1070
  }
738
1071
  // If single type, downstream checks can skip guards
739
1072
  if (types.length === 1) effectiveType = types[0]
@@ -1050,14 +1383,31 @@ function genCode(schema, v, lines, ctx, knownType) {
1050
1383
  if (schema.properties) {
1051
1384
  for (const [key, prop] of Object.entries(schema.properties)) {
1052
1385
  if (requiredSet.has(key) && isObj) {
1053
- // Required + type:object — property exists, use destructured local
1054
- genCode(prop, hoisted[key] || `${v}[${JSON.stringify(key)}]`, lines, ctx)
1386
+ // Required + type:object — hoist to local to reduce repeated property lookups
1387
+ const access = hoisted[key] || `${v}[${JSON.stringify(key)}]`
1388
+ const combined = tryGenCombined(prop, access, ctx)
1389
+ if (combined) {
1390
+ lines.push(combined)
1391
+ } else if (needsLocal(prop)) {
1392
+ const oi = ctx.varCounter++
1393
+ const local = `_r${oi}`
1394
+ lines.push(`{const ${local}=${access}`)
1395
+ genCode(prop, local, lines, ctx)
1396
+ lines.push(`}`)
1397
+ } else {
1398
+ genCode(prop, access, lines, ctx)
1399
+ }
1055
1400
  } else if (isObj) {
1056
1401
  // Optional — hoist to local, check undefined
1057
1402
  const oi = ctx.varCounter++
1058
1403
  const local = `_o${oi}`
1059
1404
  lines.push(`{const ${local}=${v}[${JSON.stringify(key)}];if(${local}!==undefined){`)
1060
- genCode(prop, local, lines, ctx)
1405
+ const combined = tryGenCombined(prop, local, ctx)
1406
+ if (combined) {
1407
+ lines.push(combined)
1408
+ } else {
1409
+ genCode(prop, local, lines, ctx)
1410
+ }
1061
1411
  lines.push(`}}`)
1062
1412
  } else {
1063
1413
  lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${JSON.stringify(key)} in ${v}){`)
@@ -1081,15 +1431,15 @@ function genCode(schema, v, lines, ctx, knownType) {
1081
1431
 
1082
1432
  // prefixItems
1083
1433
  if (schema.prefixItems) {
1434
+ const pfxVar = ctx.varCounter++
1084
1435
  for (let i = 0; i < schema.prefixItems.length; i++) {
1085
- const elem = `_p${ctx.varCounter}_${i}`
1436
+ const elem = `_p${pfxVar}_${i}`
1086
1437
  lines.push(isArr
1087
1438
  ? `if(${v}.length>${i}){const ${elem}=${v}[${i}]`
1088
1439
  : `if(Array.isArray(${v})&&${v}.length>${i}){const ${elem}=${v}[${i}]`)
1089
1440
  genCode(schema.prefixItems[i], elem, lines, ctx)
1090
1441
  lines.push(`}`)
1091
1442
  }
1092
- ctx.varCounter++
1093
1443
  }
1094
1444
 
1095
1445
  // contains — use helper function to avoid try/catch overhead
@@ -1401,23 +1751,73 @@ function genCode(schema, v, lines, ctx, knownType) {
1401
1751
  const inner = `for(var _k in ${v}){if(!${evVar}[_k])return false}`
1402
1752
  if (!ctx.deferredChecks) ctx.deferredChecks = []
1403
1753
  ctx.deferredChecks.push(isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
1404
- } else if (schema.patternProperties) {
1405
- // patternProperties: runtime key matching
1406
- const ei = ctx.varCounter++
1407
- const evVar = `_ev${ei}`
1408
- lines.push(`{const ${evVar}={}`)
1409
- for (const k of baseProps) lines.push(`${evVar}[${JSON.stringify(k)}]=1`)
1410
- const patterns = Object.keys(schema.patternProperties)
1411
- const reVars = []
1412
- for (const pat of patterns) {
1413
- const ri = ctx.varCounter++
1414
- ctx.closureVars.push(`_ure${ri}`)
1415
- ctx.closureVals.push(new RegExp(pat))
1416
- reVars.push(`_ure${ri}`)
1754
+ } else {
1755
+ // General fallback: collect all patternProperties from root + allOf sub-schemas + if
1756
+ // and use runtime regex matching
1757
+ const allPatterns = []
1758
+ if (schema.patternProperties) {
1759
+ allPatterns.push(...Object.keys(schema.patternProperties))
1760
+ }
1761
+ if (schema.allOf) {
1762
+ for (const sub of schema.allOf) {
1763
+ if (sub && sub.patternProperties) {
1764
+ allPatterns.push(...Object.keys(sub.patternProperties))
1765
+ }
1766
+ }
1767
+ }
1768
+ // lone if (no then/else) still contributes annotations when it passes
1769
+ if (schema.if && !schema.then && !schema.else && schema.if.patternProperties) {
1770
+ allPatterns.push(...Object.keys(schema.if.patternProperties))
1771
+ }
1772
+ if (allPatterns.length > 0) {
1773
+ const ei = ctx.varCounter++
1774
+ const evVar = `_ev${ei}`
1775
+ lines.push(`{const ${evVar}={}`)
1776
+ for (const k of baseProps) lines.push(`${evVar}[${JSON.stringify(k)}]=1`)
1777
+ const reVars = []
1778
+ for (const pat of allPatterns) {
1779
+ const ri = ctx.varCounter++
1780
+ ctx.closureVars.push(`_ure${ri}`)
1781
+ ctx.closureVals.push(new RegExp(pat))
1782
+ reVars.push(`_ure${ri}`)
1783
+ }
1784
+ if (schema.if && !schema.then && !schema.else) {
1785
+ // Lone if: run the if check first; if it passes, its patternProperties contribute
1786
+ const ifLines2 = []
1787
+ genCode(schema.if, '_iv2', ifLines2, ctx)
1788
+ const ufi = ctx.varCounter++
1789
+ const ifFn = ifLines2.length === 0
1790
+ ? `function(_iv2){return true}`
1791
+ : `function(_iv2){${ifLines2.join(';')};return true}`
1792
+ // Mark keys matching if's patterns as evaluated only when if passes
1793
+ const ifPatterns = schema.if.patternProperties ? Object.keys(schema.if.patternProperties) : []
1794
+ const ifReVars = []
1795
+ for (const pat of ifPatterns) {
1796
+ const ri = ctx.varCounter++
1797
+ ctx.closureVars.push(`_ure${ri}`)
1798
+ ctx.closureVals.push(new RegExp(pat))
1799
+ ifReVars.push(`_ure${ri}`)
1800
+ }
1801
+ const rootReVars = []
1802
+ if (schema.patternProperties) {
1803
+ for (const pat of Object.keys(schema.patternProperties)) {
1804
+ const ri = ctx.varCounter++
1805
+ ctx.closureVars.push(`_ure${ri}`)
1806
+ ctx.closureVals.push(new RegExp(pat))
1807
+ rootReVars.push(`_ure${ri}`)
1808
+ }
1809
+ }
1810
+ const rootPatCheck = rootReVars.map(rv => `if(${rv}.test(_k))continue;`).join('')
1811
+ const ifPatCheck = ifReVars.map(rv => `if(${rv}.test(_k))continue;`).join('')
1812
+ const inner = `const _uif${ufi}=${ifFn};if(_uif${ufi}(${v})){for(var _k in ${v}){if(${evVar}[_k])continue;${rootPatCheck}${ifPatCheck}return false}}else{for(var _k in ${v}){if(${evVar}[_k])continue;${rootPatCheck}return false}}`
1813
+ if (!ctx.deferredChecks) ctx.deferredChecks = []
1814
+ ctx.deferredChecks.push(isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
1815
+ } else {
1816
+ const inner = `for(var _k in ${v}){if(${evVar}[_k])continue;${reVars.map(rv => `if(${rv}.test(_k)){${evVar}[_k]=1;continue}`).join('')}return false}`
1817
+ if (!ctx.deferredChecks) ctx.deferredChecks = []
1818
+ ctx.deferredChecks.push(isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
1819
+ }
1417
1820
  }
1418
- const inner = `for(var _k in ${v}){if(${evVar}[_k])continue;${reVars.map(rv => `if(${rv}.test(_k)){${evVar}[_k]=1;continue}`).join('')}return false}`
1419
- if (!ctx.deferredChecks) ctx.deferredChecks = []
1420
- ctx.deferredChecks.push(isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
1421
1821
  }
1422
1822
  } else if (typeof schema.unevaluatedProperties === 'object') {
1423
1823
  // Tier 3 with schema: validate unknown keys against sub-schema
@@ -1469,7 +1869,12 @@ function genCode(schema, v, lines, ctx, knownType) {
1469
1869
  if (schema.unevaluatedItems !== undefined) {
1470
1870
  const evalResult = collectEvaluated(schema, ctx.schemaMap, ctx.rootDefs)
1471
1871
 
1472
- if (evalResult.allItems || schema.unevaluatedItems === true) {
1872
+ // Check if allItems from anyOf/oneOf branches with `items` keyword needs dynamic tracking
1873
+ const branchKw = schema.anyOf ? 'anyOf' : schema.oneOf ? 'oneOf' : null
1874
+ const hasConditionalItems = evalResult.allItems && evalResult.dynamic && branchKw &&
1875
+ schema[branchKw].some(sub => sub && typeof sub === 'object' && ((sub.items && typeof sub.items === 'object') || sub.items === true))
1876
+
1877
+ if (schema.unevaluatedItems === true || (evalResult.allItems && !hasConditionalItems)) {
1473
1878
  // All items evaluated or unevaluatedItems:true — no-op
1474
1879
  } else if (!evalResult.dynamic) {
1475
1880
  // Static: all evaluated items known at compile-time
@@ -1495,16 +1900,29 @@ function genCode(schema, v, lines, ctx, knownType) {
1495
1900
  }
1496
1901
  } else {
1497
1902
  // Dynamic: runtime tracking of max evaluated index
1498
- const baseIdx = evalResult.items || 0
1903
+ // Compute baseIdx from unconditional sources only (root prefixItems/items, allOf)
1904
+ let baseIdx = 0
1905
+ if (schema.prefixItems) baseIdx = Math.max(baseIdx, schema.prefixItems.length)
1906
+ if (schema.items && typeof schema.items === 'object') baseIdx = Infinity // items: schema → all evaluated
1907
+ if (schema.allOf) {
1908
+ for (const sub of schema.allOf) {
1909
+ const subR = collectEvaluated(sub, ctx.schemaMap, ctx.rootDefs)
1910
+ if (subR.items !== null) baseIdx = Math.max(baseIdx, subR.items)
1911
+ if (subR.allItems) baseIdx = Infinity
1912
+ }
1913
+ }
1914
+ if (baseIdx === Infinity) baseIdx = 0 // allItems already handled above
1499
1915
  const branchKeyword = schema.anyOf ? 'anyOf' : schema.oneOf ? 'oneOf' : null
1500
1916
 
1501
1917
  if (branchKeyword && (schema.unevaluatedItems === false || typeof schema.unevaluatedItems === 'object')) {
1502
1918
  // anyOf/oneOf: each branch may evaluate different number of items
1503
1919
  const branches = schema[branchKeyword]
1504
1920
  const branchMaxIdx = []
1921
+ const branchAllItems = []
1505
1922
  for (const sub of branches) {
1506
1923
  const subR = collectEvaluated(sub, ctx.schemaMap, ctx.rootDefs)
1507
1924
  branchMaxIdx.push(subR.items || 0)
1925
+ branchAllItems.push(subR.allItems)
1508
1926
  }
1509
1927
  // Runtime: find max evaluated index across all matching branches
1510
1928
  const fns = []
@@ -1518,7 +1936,10 @@ function genCode(schema, v, lines, ctx, knownType) {
1518
1936
  const evVar = `_eidx${ei}`
1519
1937
  lines.push(`{let ${evVar}=${baseIdx}`)
1520
1938
  lines.push(`const _bf${bfi}=[${fns.join(',')}]`)
1521
- const maxExprs = branchMaxIdx.map((m, i) => `_bi===${i}?${Math.max(m, baseIdx)}`).join(':') + `:${baseIdx}`
1939
+ const maxExprs = branchMaxIdx.map((m, i) => {
1940
+ if (branchAllItems[i]) return `_bi===${i}?${v}.length`
1941
+ return `_bi===${i}?${Math.max(m, baseIdx)}`
1942
+ }).join(':') + `:${baseIdx}`
1522
1943
  if (branchKeyword === 'oneOf') {
1523
1944
  lines.push(`for(let _bi=0;_bi<_bf${bfi}.length;_bi++){if(_bf${bfi}[_bi](${v})){${evVar}=${maxExprs};break}}`)
1524
1945
  } else {
@@ -1543,8 +1964,8 @@ function genCode(schema, v, lines, ctx, knownType) {
1543
1964
  lines.push('}')
1544
1965
  }
1545
1966
  }
1546
- } else if (schema.if && (schema.then || schema.else) && (schema.unevaluatedItems === false || typeof schema.unevaluatedItems === 'object')) {
1547
- // if/then/else: branch-specific max index
1967
+ } else if (schema.if && (schema.unevaluatedItems === false || typeof schema.unevaluatedItems === 'object')) {
1968
+ // if/then/else (or lone if): branch-specific max index
1548
1969
  const ifEval = collectEvaluated(schema.if, ctx.schemaMap, ctx.rootDefs)
1549
1970
  const thenEval = schema.then ? collectEvaluated(schema.then, ctx.schemaMap, ctx.rootDefs) : { items: null }
1550
1971
  const elseEval = schema.else ? collectEvaluated(schema.else, ctx.schemaMap, ctx.rootDefs) : { items: null }
@@ -1563,6 +1984,54 @@ function genCode(schema, v, lines, ctx, knownType) {
1563
1984
  const guard = isArr ? '' : `if(Array.isArray(${v}))`
1564
1985
  lines.push(`${guard}{const _uif${ufi}=${ifFn3};if(_uif${ufi}(${v})){if(${v}.length>${thenIdx})return false}else{if(${v}.length>${elseIdx})return false}}`)
1565
1986
  }
1987
+ } else if ((schema.contains || (schema.allOf && schema.allOf.some(s => s && s.contains))) && (schema.unevaluatedItems === false || typeof schema.unevaluatedItems === 'object')) {
1988
+ // contains + unevaluatedItems: per-item tracking of which items are matched by contains
1989
+ // Collect contains from root and allOf sub-schemas
1990
+ const allContains = []
1991
+ if (schema.contains) allContains.push(schema.contains)
1992
+ if (schema.allOf) {
1993
+ for (const sub of schema.allOf) {
1994
+ if (sub && sub.contains) allContains.push(sub.contains)
1995
+ }
1996
+ }
1997
+ const ci = ctx.varCounter++
1998
+ const evArr = `_cev${ci}`
1999
+ const containsFns = []
2000
+ for (const c of allContains) {
2001
+ const cLines = []
2002
+ genCode(c, '_cv', cLines, ctx)
2003
+ containsFns.push(cLines.length === 0
2004
+ ? `function(_cv){return true}`
2005
+ : `function(_cv){${cLines.join(';')};return true}`)
2006
+ }
2007
+ const cfnArr = `_cfn${ci}`
2008
+ lines.push(`{const ${cfnArr}=[${containsFns.join(',')}]`)
2009
+ // Mark items evaluated by prefixItems
2010
+ lines.push(`const ${evArr}=[]`)
2011
+ if (baseIdx > 0) {
2012
+ lines.push(`for(let _i=0;_i<${Math.min(baseIdx, 1000)};_i++)${evArr}[_i]=true`)
2013
+ }
2014
+ // Mark items matched by each contains function
2015
+ lines.push(`if(Array.isArray(${v})){for(let _ci=0;_ci<${v}.length;_ci++){for(let _cj=0;_cj<${cfnArr}.length;_cj++){if(${cfnArr}[_cj](${v}[_ci])){${evArr}[_ci]=true;break}}}}`)
2016
+ if (schema.unevaluatedItems === false) {
2017
+ const inner = `if(Array.isArray(${v})){for(let _ci=0;_ci<${v}.length;_ci++){if(!${evArr}[_ci])return false}}`
2018
+ if (!ctx.deferredChecks) ctx.deferredChecks = []
2019
+ ctx.deferredChecks.push(inner + '}')
2020
+ } else {
2021
+ // unevaluatedItems: {schema}
2022
+ const ui = ctx.varCounter++
2023
+ const elemVar = `_ue${ui}`
2024
+ const subLines = []
2025
+ genCode(schema.unevaluatedItems, elemVar, subLines, ctx)
2026
+ if (subLines.length > 0) {
2027
+ const check = subLines.join(';')
2028
+ const inner = `if(Array.isArray(${v})){for(let _ci=0;_ci<${v}.length;_ci++){if(!${evArr}[_ci]){const ${elemVar}=${v}[_ci];${check}}}}`
2029
+ if (!ctx.deferredChecks) ctx.deferredChecks = []
2030
+ ctx.deferredChecks.push(inner + '}')
2031
+ } else {
2032
+ lines.push('}')
2033
+ }
2034
+ }
1566
2035
  } else if (schema.unevaluatedItems === false) {
1567
2036
  // Fallback: use static base index (may not be fully correct for all dynamic cases)
1568
2037
  const maxIdx = evalResult.items || 0
@@ -1582,8 +2051,8 @@ const FORMAT_CODEGEN = {
1582
2051
  : `if(typeof ${v}==='string'){const _at=${v}.indexOf('@');if(_at<=0||_at>=${v}.length-1||${v}.indexOf('.',_at)<=_at+1)return false}`
1583
2052
  },
1584
2053
  date: (v, isStr) => isStr
1585
- ? `if(${v}.length!==10||!/^\\d{4}-\\d{2}-\\d{2}$/.test(${v}))return false`
1586
- : `if(typeof ${v}==='string'&&(${v}.length!==10||!/^\\d{4}-\\d{2}-\\d{2}$/.test(${v})))return false`,
2054
+ ? `{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}`
2055
+ : `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}`,
1587
2056
  uuid: (v, isStr) => isStr
1588
2057
  ? `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`
1589
2058
  : `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`,
@@ -1741,6 +2210,8 @@ function compileToJSCodegenWithErrors(schema, schemaMap) {
1741
2210
  if (typeof schema === 'object' && schema !== null) {
1742
2211
  const s = JSON.stringify(schema)
1743
2212
  if (s.includes('unevaluatedProperties') || s.includes('unevaluatedItems')) return null
2213
+ // Bail on self-referencing schemas — error codegen doesn't support recursion
2214
+ if (s.includes('"$ref":"#"')) return null
1744
2215
  }
1745
2216
  if (typeof schema === 'boolean') {
1746
2217
  return schema
@@ -1770,15 +2241,46 @@ function compileToJSCodegenWithErrors(schema, schemaMap) {
1770
2241
  if (keys.some(k => !supported.includes(k))) return null
1771
2242
  }
1772
2243
 
1773
- const ctx = { varCounter: 0, helperCode: [], rootDefs: schema.$defs || schema.definitions || null, refStack: new Set(), schemaMap: schemaMap || null }
2244
+ // Build anchors map for $ref/#anchor and $dynamicRef resolution
2245
+ const eRootDefs = schema.$defs || schema.definitions || null
2246
+ const eAnchors = {}
2247
+ if (schema.$dynamicAnchor) eAnchors['#' + schema.$dynamicAnchor] = schema
2248
+ if (schema.$anchor) eAnchors['#' + schema.$anchor] = schema
2249
+ if (eRootDefs) {
2250
+ for (const def of Object.values(eRootDefs)) {
2251
+ if (def && typeof def === 'object') {
2252
+ if (def.$dynamicAnchor) eAnchors['#' + def.$dynamicAnchor] = def
2253
+ if (def.$anchor) eAnchors['#' + def.$anchor] = def
2254
+ }
2255
+ }
2256
+ }
2257
+ if (schemaMap) {
2258
+ for (const ext of schemaMap.values()) {
2259
+ if (ext && typeof ext === 'object') {
2260
+ if (ext.$dynamicAnchor && !eAnchors['#' + ext.$dynamicAnchor]) eAnchors['#' + ext.$dynamicAnchor] = ext
2261
+ if (ext.$anchor && !eAnchors['#' + ext.$anchor]) eAnchors['#' + ext.$anchor] = ext
2262
+ }
2263
+ }
2264
+ }
2265
+
2266
+ const ctx = { varCounter: 0, helperCode: [], rootDefs: eRootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors: eAnchors, rootSchema: schema }
1774
2267
  const lines = []
1775
2268
  genCodeE(schema, 'd', '', lines, ctx, '#')
1776
2269
  if (lines.length === 0) return (d) => ({ valid: true, errors: [] })
1777
2270
 
1778
- const body = `const _e=[];\n ` +
1779
- (ctx.helperCode.length ? ctx.helperCode.join('\n ') + '\n ' : '') +
1780
- lines.join('\n ') +
1781
- `\n return{valid:_e.length===0,errors:_e}`
2271
+ const checkStr = lines.join('\n ')
2272
+ let body
2273
+ if (ctx.usesRecursion) {
2274
+ body = `const _e=[];\n ` +
2275
+ (ctx.helperCode.length ? ctx.helperCode.join('\n ') + '\n ' : '') +
2276
+ `function _validateE(d,_all,_e){\n ${checkStr}\n }\n _validateE(d,_all,_e);\n ` +
2277
+ `return{valid:_e.length===0,errors:_e}`
2278
+ } else {
2279
+ body = `const _e=[];\n ` +
2280
+ (ctx.helperCode.length ? ctx.helperCode.join('\n ') + '\n ' : '') +
2281
+ checkStr +
2282
+ `\n return{valid:_e.length===0,errors:_e}`
2283
+ }
1782
2284
  try {
1783
2285
  const fn = new Function('d', '_all', body)
1784
2286
  fn._errSource = body
@@ -1797,6 +2299,8 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
1797
2299
 
1798
2300
  // $ref — resolve local and cross-schema refs
1799
2301
  if (schema.$ref) {
2302
+ // Self-reference "#" — no-op (permissive) to avoid infinite recursion
2303
+ if (schema.$ref === '#') return
1800
2304
  const m = schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
1801
2305
  if (m && ctx.rootDefs && ctx.rootDefs[m[1]]) {
1802
2306
  if (ctx.refStack.has(schema.$ref)) return
@@ -1805,6 +2309,18 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
1805
2309
  ctx.refStack.delete(schema.$ref)
1806
2310
  return
1807
2311
  }
2312
+ // Anchor ref: "#foo" — resolve via rootDefs or anchors map
2313
+ if (!m && schema.$ref.startsWith('#') && !schema.$ref.startsWith('#/')) {
2314
+ const entry = ctx.rootDefs && ctx.rootDefs[schema.$ref]
2315
+ const anchorTarget = entry && entry.raw ? entry.raw : (ctx.anchors && ctx.anchors[schema.$ref])
2316
+ if (anchorTarget) {
2317
+ if (ctx.refStack.has(schema.$ref)) return
2318
+ ctx.refStack.add(schema.$ref)
2319
+ genCodeE(anchorTarget, v, pathExpr, lines, ctx, schemaPrefix)
2320
+ ctx.refStack.delete(schema.$ref)
2321
+ return
2322
+ }
2323
+ }
1808
2324
  if (ctx.schemaMap && ctx.schemaMap.has(schema.$ref)) {
1809
2325
  if (ctx.refStack.has(schema.$ref)) return
1810
2326
  ctx.refStack.add(schema.$ref)
@@ -1814,6 +2330,26 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
1814
2330
  }
1815
2331
  }
1816
2332
 
2333
+ // $dynamicRef — resolve via anchors map
2334
+ if (schema.$dynamicRef) {
2335
+ const anchorKey = schema.$dynamicRef.startsWith('#') ? schema.$dynamicRef : '#' + schema.$dynamicRef
2336
+ if (ctx.anchors && ctx.anchors[anchorKey]) {
2337
+ const target = ctx.anchors[anchorKey]
2338
+ if (target === ctx.rootSchema) {
2339
+ // Self-recursive: generate _validateE call
2340
+ ctx.usesRecursion = true
2341
+ lines.push(`_validateE(${v},_all,_e)`)
2342
+ } else {
2343
+ const refKey = '$dynamicRef:' + anchorKey
2344
+ if (!ctx.refStack.has(refKey)) {
2345
+ ctx.refStack.add(refKey)
2346
+ genCodeE(target, v, pathExpr, lines, ctx, schemaPrefix)
2347
+ ctx.refStack.delete(refKey)
2348
+ }
2349
+ }
2350
+ }
2351
+ }
2352
+
1817
2353
  const types = schema.type ? (Array.isArray(schema.type) ? schema.type : [schema.type]) : null
1818
2354
  if (types) {
1819
2355
  const conds = types.map(t => {
@@ -1823,7 +2359,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
1823
2359
  case 'string': return `typeof ${v}==='string'`
1824
2360
  case 'number': return `(typeof ${v}==='number'&&isFinite(${v}))`
1825
2361
  case 'integer': return `Number.isInteger(${v})`
1826
- case 'boolean': return `(${v}===true||${v}===false)`
2362
+ case 'boolean': return `typeof ${v}==='boolean'`
1827
2363
  case 'null': return `${v}===null`
1828
2364
  default: return 'true'
1829
2365
  }
@@ -1864,8 +2400,8 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
1864
2400
  const ci = ctx.varCounter++
1865
2401
  const canonFn = `_cnE${ci}`
1866
2402
  ctx.helperCode.push(`const ${canonFn}=function(x){if(x===null||typeof x!=='object')return JSON.stringify(x);if(Array.isArray(x))return'['+x.map(${canonFn}).join(',')+']';return'{'+Object.keys(x).sort().map(function(k){return JSON.stringify(k)+':'+${canonFn}(x[k])}).join(',')+'}'};`)
1867
- const expected = canonFn + '(' + JSON.stringify(cv) + ')'
1868
- lines.push(`if(${canonFn}(${v})!==${expected}){${fail('const', 'const', `{allowedValue:${JSON.stringify(schema.const)}}`, "'must be equal to constant'")}}`)
2403
+ const expected = canonFn + '(JSON.parse(' + JSON.stringify(JSON.stringify(cv)) + '))'
2404
+ lines.push(`if(${canonFn}(${v})!==${expected}){${fail('const', 'const', `{allowedValue:JSON.parse(${JSON.stringify(JSON.stringify(schema.const))})}`, "'must be equal to constant'")}}`)
1869
2405
  }
1870
2406
  }
1871
2407
 
@@ -2157,6 +2693,8 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap) {
2157
2693
  if (typeof schema === 'object' && schema !== null) {
2158
2694
  const s = JSON.stringify(schema)
2159
2695
  if (s.includes('unevaluatedProperties') || s.includes('unevaluatedItems')) return null
2696
+ // Bail on self-referencing schemas — combined codegen doesn't support recursion
2697
+ if (s.includes('"$ref":"#"')) return null
2160
2698
  }
2161
2699
  if (typeof schema === 'boolean') {
2162
2700
  return schema
@@ -2186,8 +2724,30 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap) {
2186
2724
  if (keys.some(k => !supported.includes(k))) return null
2187
2725
  }
2188
2726
 
2727
+ // Build anchors map for $ref/#anchor and $dynamicRef resolution
2728
+ const cRootDefs = schema.$defs || schema.definitions || null
2729
+ const cAnchors = {}
2730
+ if (schema.$dynamicAnchor) cAnchors['#' + schema.$dynamicAnchor] = schema
2731
+ if (schema.$anchor) cAnchors['#' + schema.$anchor] = schema
2732
+ if (cRootDefs) {
2733
+ for (const def of Object.values(cRootDefs)) {
2734
+ if (def && typeof def === 'object') {
2735
+ if (def.$dynamicAnchor) cAnchors['#' + def.$dynamicAnchor] = def
2736
+ if (def.$anchor) cAnchors['#' + def.$anchor] = def
2737
+ }
2738
+ }
2739
+ }
2740
+ if (schemaMap) {
2741
+ for (const ext of schemaMap.values()) {
2742
+ if (ext && typeof ext === 'object') {
2743
+ if (ext.$dynamicAnchor && !cAnchors['#' + ext.$dynamicAnchor]) cAnchors['#' + ext.$dynamicAnchor] = ext
2744
+ if (ext.$anchor && !cAnchors['#' + ext.$anchor]) cAnchors['#' + ext.$anchor] = ext
2745
+ }
2746
+ }
2747
+ }
2748
+
2189
2749
  const ctx = { varCounter: 0, helperCode: [], closureVars: [], closureVals: [],
2190
- rootDefs: schema.$defs || schema.definitions || null, refStack: new Set(), schemaMap: schemaMap || null }
2750
+ rootDefs: cRootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors: cAnchors, rootSchema: schema }
2191
2751
  const lines = []
2192
2752
  genCodeC(schema, 'd', '', lines, ctx, '#')
2193
2753
  if (lines.length === 0) return () => VALID_RESULT
@@ -2218,8 +2778,10 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
2218
2778
  if (!schemaPrefix) schemaPrefix = '#'
2219
2779
  if (typeof schema !== 'object' || schema === null) return
2220
2780
 
2221
- // $ref — resolve local and cross-schema refs
2781
+ // $ref — resolve local, anchor, and cross-schema refs
2222
2782
  if (schema.$ref) {
2783
+ // Self-reference "#" — no-op (permissive) to avoid infinite recursion
2784
+ if (schema.$ref === '#') return
2223
2785
  const m = schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
2224
2786
  if (m && ctx.rootDefs && ctx.rootDefs[m[1]]) {
2225
2787
  if (ctx.refStack.has(schema.$ref)) return
@@ -2228,6 +2790,18 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
2228
2790
  ctx.refStack.delete(schema.$ref)
2229
2791
  return
2230
2792
  }
2793
+ // Anchor ref: "#foo" — resolve via rootDefs or anchors map
2794
+ if (!m && schema.$ref.startsWith('#') && !schema.$ref.startsWith('#/')) {
2795
+ const entry = ctx.rootDefs && ctx.rootDefs[schema.$ref]
2796
+ const anchorTarget = entry && entry.raw ? entry.raw : (ctx.anchors && ctx.anchors[schema.$ref])
2797
+ if (anchorTarget) {
2798
+ if (ctx.refStack.has(schema.$ref)) return
2799
+ ctx.refStack.add(schema.$ref)
2800
+ genCodeC(anchorTarget, v, pathExpr, lines, ctx, schemaPrefix)
2801
+ ctx.refStack.delete(schema.$ref)
2802
+ return
2803
+ }
2804
+ }
2231
2805
  if (ctx.schemaMap && ctx.schemaMap.has(schema.$ref)) {
2232
2806
  if (ctx.refStack.has(schema.$ref)) return
2233
2807
  ctx.refStack.add(schema.$ref)
@@ -2237,6 +2811,25 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
2237
2811
  }
2238
2812
  }
2239
2813
 
2814
+ // $dynamicRef — resolve via anchors map
2815
+ if (schema.$dynamicRef) {
2816
+ const anchorKey = schema.$dynamicRef.startsWith('#') ? schema.$dynamicRef : '#' + schema.$dynamicRef
2817
+ if (ctx.anchors && ctx.anchors[anchorKey]) {
2818
+ const target = ctx.anchors[anchorKey]
2819
+ if (target === ctx.rootSchema) {
2820
+ // Self-recursive: bail to non-combined path (combined doesn't support named recursion)
2821
+ // Just skip — the hybrid path will handle this via jsFn + errFn
2822
+ } else {
2823
+ const refKey = '$dynamicRef:' + anchorKey
2824
+ if (!ctx.refStack.has(refKey)) {
2825
+ ctx.refStack.add(refKey)
2826
+ genCodeC(target, v, pathExpr, lines, ctx, schemaPrefix)
2827
+ ctx.refStack.delete(refKey)
2828
+ }
2829
+ }
2830
+ }
2831
+ }
2832
+
2240
2833
  const types = schema.type ? (Array.isArray(schema.type) ? schema.type : [schema.type]) : null
2241
2834
  let isObj = false, isArr = false, isStr = false, isNum = false
2242
2835
 
@@ -2272,7 +2865,7 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
2272
2865
  case 'string': return `typeof ${v}==='string'`
2273
2866
  case 'number': return `(typeof ${v}==='number'&&isFinite(${v}))`
2274
2867
  case 'integer': return `Number.isInteger(${v})`
2275
- case 'boolean': return `(${v}===true||${v}===false)`
2868
+ case 'boolean': return `typeof ${v}==='boolean'`
2276
2869
  case 'null': return `${v}===null`
2277
2870
  default: return 'true'
2278
2871
  }
@@ -2312,7 +2905,7 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
2312
2905
  const ci = ctx.varCounter++
2313
2906
  const canonFn = `_cn${ci}`
2314
2907
  ctx.helperCode.push(`const ${canonFn}=function(x){if(x===null||typeof x!=='object')return JSON.stringify(x);if(Array.isArray(x))return'['+x.map(${canonFn}).join(',')+']';return'{'+Object.keys(x).sort().map(function(k){return JSON.stringify(k)+':'+${canonFn}(x[k])}).join(',')+'}'};`)
2315
- lines.push(`if(${canonFn}(${v})!==${canonFn}(${JSON.stringify(cv)})){${fail('const', 'const', `{allowedValue:${JSON.stringify(schema.const)}}`, "'must be equal to constant'")}}`)
2908
+ lines.push(`if(${canonFn}(${v})!==${canonFn}(JSON.parse(${JSON.stringify(JSON.stringify(cv))}))){${fail('const', 'const', `{allowedValue:JSON.parse(${JSON.stringify(JSON.stringify(schema.const))})}`, "'must be equal to constant'")}}`)
2316
2909
  }
2317
2910
  }
2318
2911
 
@@ -2729,11 +3322,20 @@ function _collectEval(schema, result, defs, schemaMap, refStack, isRoot) {
2729
3322
  refStack.add(schema.$ref)
2730
3323
  _collectEval(defs[m[1]], result, defs, schemaMap, refStack)
2731
3324
  refStack.delete(schema.$ref)
2732
- } else if (schemaMap && typeof schemaMap.get === 'function' && schemaMap.has(schema.$ref)) {
2733
- if (refStack.has(schema.$ref)) { result.dynamic = true; return }
2734
- refStack.add(schema.$ref)
2735
- _collectEval(schemaMap.get(schema.$ref), result, defs, schemaMap, refStack)
2736
- refStack.delete(schema.$ref)
3325
+ } else if (schemaMap && typeof schemaMap.get === 'function') {
3326
+ let resolved = schemaMap.has(schema.$ref) ? schemaMap.get(schema.$ref) : null
3327
+ // Relative URI resolution
3328
+ if (!resolved && !schema.$ref.includes('://') && !schema.$ref.startsWith('#')) {
3329
+ for (const [id, s] of schemaMap) {
3330
+ if (id.endsWith('/' + schema.$ref)) { resolved = s; break }
3331
+ }
3332
+ }
3333
+ if (resolved) {
3334
+ if (refStack.has(schema.$ref)) { result.dynamic = true; return }
3335
+ refStack.add(schema.$ref)
3336
+ _collectEval(resolved, result, defs, schemaMap, refStack)
3337
+ refStack.delete(schema.$ref)
3338
+ }
2737
3339
  }
2738
3340
  // In 2020-12, $ref can coexist with siblings — don't return early if there are other keywords
2739
3341
  const hasOtherKeywords = Object.keys(schema).some(k => k !== '$ref' && k !== '$defs' && k !== 'definitions' && k !== '$schema' && k !== '$id')
@@ -2771,15 +3373,10 @@ function _collectEval(schema, result, defs, schemaMap, refStack, isRoot) {
2771
3373
  result.allItems = true
2772
3374
  }
2773
3375
 
2774
- // contains interaction with unevaluatedItems is complex
2775
- // At root level: contains + unevaluatedItems needs dynamic tracking
2776
- // In nested schemas: contains marks all items as evaluated
3376
+ // contains: marks matching items as evaluated (not ALL items)
3377
+ // Always set dynamic since which items match depends on the data
2777
3378
  if (schema.contains) {
2778
- if (isRoot && (schema.unevaluatedItems !== undefined)) {
2779
- result.dynamic = true
2780
- } else {
2781
- result.allItems = true
2782
- }
3379
+ result.dynamic = true
2783
3380
  }
2784
3381
 
2785
3382
  // unevaluatedProperties: true/schema → all props evaluated (for nested schemas only)
@@ -2814,6 +3411,18 @@ function _collectEval(schema, result, defs, schemaMap, refStack, isRoot) {
2814
3411
  _collectEval(schema.if, result, defs, schemaMap, refStack)
2815
3412
  if (schema.then) _collectEval(schema.then, result, defs, schemaMap, refStack)
2816
3413
  if (schema.else) _collectEval(schema.else, result, defs, schemaMap, refStack)
3414
+ } else if (schema.if) {
3415
+ // Standalone if (no then/else) still produces annotations per spec
3416
+ // Only collect properties and patterns, not deep items (contains etc.)
3417
+ result.dynamic = true
3418
+ if (schema.if.properties) {
3419
+ for (const k of Object.keys(schema.if.properties)) {
3420
+ if (!result.props.includes(k)) result.props.push(k)
3421
+ }
3422
+ }
3423
+ if (schema.if.patternProperties) {
3424
+ // patternProperties contribute to dynamic evaluation
3425
+ }
2817
3426
  }
2818
3427
 
2819
3428
  // dependentSchemas → dynamic