schema-shield 1.0.5 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.ts CHANGED
@@ -2,8 +2,10 @@
2
2
  import {
3
3
  DefineErrorFunction,
4
4
  ValidationError,
5
+ definePropertyOrThrow,
5
6
  getDefinedErrorFunctionForKey,
6
7
  getNamedFunction,
8
+ hasOwn,
7
9
  resolvePath
8
10
  } from "./utils/main-utils";
9
11
 
@@ -11,18 +13,31 @@ import { Formats } from "./formats";
11
13
  import { Types } from "./types";
12
14
  import { keywords } from "./keywords";
13
15
  import { deepCloneUnfreeze } from "./utils/deep-freeze";
16
+ import {
17
+ createCombinatorValidator,
18
+ prepareCombinatorEntries
19
+ } from "./keywords/other-keywords";
20
+ import {
21
+ applyPropertyDefaults,
22
+ applyEmptyPropertyDefaults
23
+ } from "./keywords/object-keywords";
14
24
 
15
25
  export { ValidationError } from "./utils/main-utils";
16
26
  export { deepCloneUnfreeze as deepClone } from "./utils/deep-freeze";
17
27
 
18
28
  export type Result = void | ValidationError | true;
19
29
 
30
+ export interface ValidateSubschemaFunction {
31
+ (schema: CompiledSchema, data: any): Result;
32
+ }
33
+
20
34
  export interface KeywordFunction {
21
35
  (
22
36
  schema: CompiledSchema,
23
37
  data: any,
24
38
  defineError: DefineErrorFunction,
25
- instance: SchemaShield
39
+ instance: SchemaShield,
40
+ validateSubschema?: ValidateSubschemaFunction
26
41
  ): Result;
27
42
  }
28
43
 
@@ -57,24 +72,134 @@ interface ValidatorItem {
57
72
  validate: ValidateFunction;
58
73
  }
59
74
 
75
+ interface PendingCombinator {
76
+ item: ValidatorItem;
77
+ key: "allOf" | "anyOf" | "oneOf";
78
+ defineError: DefineErrorFunction;
79
+ }
80
+
81
+ interface ValidationContext {
82
+ active: boolean;
83
+ depth: number;
84
+ depthExceeded: boolean;
85
+ depthError?: ValidationError | true;
86
+ defaults: DefaultJournalEntry[];
87
+ }
88
+
89
+ interface SchemaAnalysis {
90
+ requiresDepthGuard: boolean;
91
+ requiresMutationJournal: boolean;
92
+ mutableSchemas: WeakSet<object>;
93
+ }
94
+
95
+ interface SchemaPosition {
96
+ source: Record<string, any>;
97
+ baseUri: string;
98
+ resourceRoot: Record<string, any>;
99
+ pointer: string;
100
+ }
101
+
102
+ interface ReferenceRegistry {
103
+ aliases: ReadonlyMap<string, Record<string, any>>;
104
+ positions: ReadonlyArray<SchemaPosition>;
105
+ positionsByNode: WeakMap<object, SchemaPosition>;
106
+ }
107
+
108
+ interface DefaultMutation {
109
+ target: Record<string, any>;
110
+ key: string;
111
+ value: any;
112
+ }
113
+
114
+ interface DefaultJournalEntry {
115
+ target: Record<string, any>;
116
+ key: string;
117
+ descriptor?: PropertyDescriptor;
118
+ }
119
+
120
+ interface DepthGuardState {
121
+ context: ValidationContext | null;
122
+ }
123
+
124
+ const MAX_COMPILE_DEPTH = 128;
125
+ const LOCAL_SCHEMA_BASE = "schema-shield://local/root";
126
+
127
+ const FAIL_FAST_TYPE_VALIDATORS: Record<string, ValidateFunction> = {
128
+ object: (data) =>
129
+ data !== null && typeof data === "object" && !Array.isArray(data)
130
+ ? undefined
131
+ : true,
132
+ array: (data) => (Array.isArray(data) ? undefined : true),
133
+ string: (data) => (typeof data === "string" ? undefined : true),
134
+ number: (data) =>
135
+ typeof data === "number" && Number.isFinite(data) ? undefined : true,
136
+ integer: (data) =>
137
+ typeof data === "number" && Number.isFinite(data) && Number.isInteger(data)
138
+ ? undefined
139
+ : true,
140
+ boolean: (data) => (typeof data === "boolean" ? undefined : true),
141
+ null: (data) => (data === null ? undefined : true)
142
+ };
143
+
144
+ function createBuiltinTypeValidator(
145
+ _type: string,
146
+ defineError: DefineErrorFunction,
147
+ fallback: TypeFunction
148
+ ): ValidateFunction {
149
+ return (data) => {
150
+ if (!fallback(data)) {
151
+ return defineError("Invalid type", { data });
152
+ }
153
+ };
154
+ }
155
+
60
156
  export class SchemaShield {
61
157
  private types: Record<string, TypeFunction | false> = {};
62
158
  private formats: Record<string, FormatFunction | false> = {};
63
159
  private keywords: Record<string, KeywordFunction | false> = {};
64
160
  private immutable = false;
161
+ private useDefaults: boolean | "empty" = false;
65
162
  private rootSchema: CompiledSchema | null = null;
66
- private idRegistry: Map<string, CompiledSchema> = new Map();
67
163
  private failFast: boolean = true;
164
+ private maxDepth: number;
165
+ private validationContexts: ValidationContext[] = [];
166
+ private compileCache: WeakMap<object, CompiledSchema> = new WeakMap();
167
+ private compilingRequiresContext = false;
168
+ private compilingMutableSchemas: WeakSet<object> = new WeakSet();
68
169
 
69
170
  constructor({
70
171
  immutable = false,
71
- failFast = true
172
+ failFast = true,
173
+ maxDepth = 128,
174
+ useDefaults = false
72
175
  }: {
73
176
  immutable?: boolean;
74
177
  failFast?: boolean;
178
+ maxDepth?: number;
179
+ useDefaults?: boolean | "empty";
75
180
  } = {}) {
181
+ if (!Number.isInteger(maxDepth) || maxDepth < 1 || maxDepth > 256) {
182
+ const error = new ValidationError("maxDepth must be an integer from 1 to 256");
183
+ error.code = "INVALID_MAX_DEPTH";
184
+ error.keyword = "maxDepth";
185
+ throw error;
186
+ }
187
+ if (
188
+ useDefaults !== false &&
189
+ useDefaults !== true &&
190
+ useDefaults !== "empty"
191
+ ) {
192
+ const error = new ValidationError(
193
+ 'useDefaults must be false, true, or "empty"'
194
+ );
195
+ error.code = "INVALID_USE_DEFAULTS";
196
+ error.keyword = "useDefaults";
197
+ throw error;
198
+ }
76
199
  this.immutable = immutable;
77
200
  this.failFast = failFast;
201
+ this.maxDepth = maxDepth;
202
+ this.useDefaults = useDefaults;
78
203
 
79
204
  for (const [type, validator] of Object.entries(Types)) {
80
205
  if (validator) {
@@ -93,6 +218,56 @@ export class SchemaShield {
93
218
  }
94
219
  }
95
220
 
221
+ setDefault(target: Record<string, any>, key: string, value: any) {
222
+ const context = this.validationContexts[this.validationContexts.length - 1];
223
+ if (context) {
224
+ context.defaults.push({
225
+ target,
226
+ key,
227
+ descriptor: Reflect.getOwnPropertyDescriptor(target, key)
228
+ });
229
+ }
230
+ definePropertyOrThrow(target, key, {
231
+ value,
232
+ enumerable: true,
233
+ configurable: true,
234
+ writable: true
235
+ });
236
+ }
237
+
238
+ #defaultSavepoint() {
239
+ const context = this.validationContexts[this.validationContexts.length - 1];
240
+ return context ? context.defaults.length : 0;
241
+ }
242
+
243
+ #rollbackDefaultSavepoint(savepoint: number) {
244
+ const context = this.validationContexts[this.validationContexts.length - 1];
245
+ if (context) {
246
+ this.rollbackDefaults(context, savepoint);
247
+ }
248
+ }
249
+
250
+ #captureDefaultSavepoint(savepoint: number): DefaultMutation[] {
251
+ const context = this.validationContexts[this.validationContexts.length - 1];
252
+ if (!context || context.defaults.length === savepoint) {
253
+ return [];
254
+ }
255
+
256
+ const mutations = context.defaults.slice(savepoint).map((entry) => ({
257
+ ...entry,
258
+ value: entry.target[entry.key]
259
+ }));
260
+ this.rollbackDefaults(context, savepoint);
261
+ return mutations;
262
+ }
263
+
264
+ #restoreDefaults(mutations: DefaultMutation[]) {
265
+ for (let index = 0; index < mutations.length; index++) {
266
+ const mutation = mutations[index];
267
+ this.setDefault(mutation.target, mutation.key, mutation.value);
268
+ }
269
+ }
270
+
96
271
  addType(name: string, validator: TypeFunction, overwrite = false) {
97
272
  if (this.types[name] && !overwrite) {
98
273
  throw new ValidationError(`Type "${name}" already exists`);
@@ -138,15 +313,570 @@ export class SchemaShield {
138
313
  }
139
314
 
140
315
  getSchemaById(id: string): CompiledSchema | undefined {
141
- return this.idRegistry.get(id);
316
+ if (!this.rootSchema) {
317
+ return;
318
+ }
319
+
320
+ const stack: CompiledSchema[] = [this.rootSchema];
321
+ const seen = new WeakSet<object>();
322
+ while (stack.length > 0) {
323
+ const node = stack.pop()!;
324
+ if (seen.has(node)) {
325
+ continue;
326
+ }
327
+ seen.add(node);
328
+ if (node.$id === id) {
329
+ return node;
330
+ }
331
+
332
+ const children = this.schemaChildren(node);
333
+ for (let index = 0; index < children.length; index++) {
334
+ stack.push(children[index] as CompiledSchema);
335
+ }
336
+ }
337
+
338
+ return;
339
+ }
340
+
341
+ private depthError(message = "Maximum schema depth exceeded") {
342
+ if (this.failFast) {
343
+ return true as const;
344
+ }
345
+ const error = new ValidationError(message);
346
+ error.code = "MAX_DEPTH_EXCEEDED";
347
+ error.keyword = "maxDepth";
348
+ return error;
349
+ }
350
+
351
+ private schemaChildEntries(
352
+ schema: Record<string, any>
353
+ ): Array<{ value: object; pointer: string }> {
354
+ const children: Array<{ value: object; pointer: string }> = [];
355
+ const mapKeys = [
356
+ "properties",
357
+ "patternProperties",
358
+ "definitions",
359
+ "$defs",
360
+ "dependencies"
361
+ ];
362
+ const arrayKeys = ["allOf", "anyOf", "oneOf", "items"];
363
+ const singleKeys = [
364
+ "items",
365
+ "additionalItems",
366
+ "additionalProperties",
367
+ "contains",
368
+ "propertyNames",
369
+ "values",
370
+ "elements",
371
+ "not",
372
+ "if",
373
+ "then",
374
+ "else"
375
+ ];
376
+
377
+ for (const key of mapKeys) {
378
+ const value = schema[key];
379
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
380
+ continue;
381
+ }
382
+ for (const childKey of Object.keys(value)) {
383
+ const child = value[childKey];
384
+ if (child && typeof child === "object") {
385
+ children.push({
386
+ value: child,
387
+ pointer: `/${this.escapePointerToken(key)}/${this.escapePointerToken(
388
+ childKey
389
+ )}`
390
+ });
391
+ }
392
+ }
393
+ }
394
+
395
+ for (const key of arrayKeys) {
396
+ const value = schema[key];
397
+ if (!Array.isArray(value)) {
398
+ continue;
399
+ }
400
+ for (let index = 0; index < value.length; index++) {
401
+ const child = value[index];
402
+ if (child && typeof child === "object") {
403
+ children.push({
404
+ value: child,
405
+ pointer: `/${this.escapePointerToken(key)}/${index}`
406
+ });
407
+ }
408
+ }
409
+ }
410
+
411
+ for (const key of singleKeys) {
412
+ const value = schema[key];
413
+ if (value && typeof value === "object" && !Array.isArray(value)) {
414
+ children.push({
415
+ value,
416
+ pointer: `/${this.escapePointerToken(key)}`
417
+ });
418
+ }
419
+ }
420
+
421
+ for (const key of Object.keys(schema)) {
422
+ if (
423
+ key === "enum" ||
424
+ key === "const" ||
425
+ key === "default" ||
426
+ key === "examples" ||
427
+ mapKeys.includes(key) ||
428
+ arrayKeys.includes(key) ||
429
+ singleKeys.includes(key)
430
+ ) {
431
+ continue;
432
+ }
433
+ const keyword = this.getKeyword(key);
434
+ const value = schema[key];
435
+ if (
436
+ keyword &&
437
+ keyword !== keywords[key] &&
438
+ value &&
439
+ typeof value === "object"
440
+ ) {
441
+ children.push({
442
+ value,
443
+ pointer: `/${this.escapePointerToken(key)}`
444
+ });
445
+ }
446
+ }
447
+
448
+ return children;
449
+ }
450
+
451
+ private schemaChildren(schema: Record<string, any>): object[] {
452
+ return this.schemaChildEntries(schema).map((entry) => entry.value);
453
+ }
454
+
455
+ private registrySubschemaEntries(
456
+ schema: Record<string, any>
457
+ ): Array<{ value: object; pointer: string }> {
458
+ const children: Array<{ value: object; pointer: string }> = [];
459
+ const mapKeys = ["properties", "patternProperties", "definitions"];
460
+ const arrayKeys = ["allOf", "anyOf", "oneOf", "items"];
461
+ const singleKeys = [
462
+ "items",
463
+ "additionalItems",
464
+ "additionalProperties",
465
+ "contains",
466
+ "propertyNames",
467
+ "not",
468
+ "if",
469
+ "then",
470
+ "else"
471
+ ];
472
+
473
+ for (const key of mapKeys) {
474
+ const value = schema[key];
475
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
476
+ continue;
477
+ }
478
+ for (const childKey of Object.keys(value)) {
479
+ const child = value[childKey];
480
+ if (child && typeof child === "object" && !Array.isArray(child)) {
481
+ children.push({
482
+ value: child,
483
+ pointer: `/${this.escapePointerToken(key)}/${this.escapePointerToken(
484
+ childKey
485
+ )}`
486
+ });
487
+ }
488
+ }
489
+ }
490
+
491
+ const dependencies = schema.dependencies;
492
+ if (
493
+ dependencies &&
494
+ typeof dependencies === "object" &&
495
+ !Array.isArray(dependencies)
496
+ ) {
497
+ for (const dependencyKey of Object.keys(dependencies)) {
498
+ const child = dependencies[dependencyKey];
499
+ if (child && typeof child === "object" && !Array.isArray(child)) {
500
+ children.push({
501
+ value: child,
502
+ pointer: `/dependencies/${this.escapePointerToken(dependencyKey)}`
503
+ });
504
+ }
505
+ }
506
+ }
507
+
508
+ for (const key of arrayKeys) {
509
+ const value = schema[key];
510
+ if (!Array.isArray(value)) {
511
+ continue;
512
+ }
513
+ for (let index = 0; index < value.length; index++) {
514
+ const child = value[index];
515
+ if (child && typeof child === "object" && !Array.isArray(child)) {
516
+ children.push({
517
+ value: child,
518
+ pointer: `/${this.escapePointerToken(key)}/${index}`
519
+ });
520
+ }
521
+ }
522
+ }
523
+
524
+ for (const key of singleKeys) {
525
+ const value = schema[key];
526
+ if (value && typeof value === "object" && !Array.isArray(value)) {
527
+ children.push({
528
+ value,
529
+ pointer: `/${this.escapePointerToken(key)}`
530
+ });
531
+ }
532
+ }
533
+
534
+ return children;
535
+ }
536
+
537
+ private escapePointerToken(value: string): string {
538
+ return value.replace(/~/g, "~0").replace(/\//g, "~1");
539
+ }
540
+
541
+ private resolveUri(reference: string, baseUri: string, keyword: "$id" | "$ref") {
542
+ try {
543
+ return new URL(reference, baseUri).href;
544
+ } catch {
545
+ const error = new ValidationError(`Invalid ${keyword} URI: ${reference}`);
546
+ error.code = keyword === "$id" ? "INVALID_SCHEMA_ID" : "REFERENCE_NOT_FOUND";
547
+ error.keyword = keyword;
548
+ throw error;
549
+ }
550
+ }
551
+
552
+ private resourceUri(uri: string): string {
553
+ const hashIndex = uri.indexOf("#");
554
+ return hashIndex === -1 ? uri : uri.slice(0, hashIndex);
555
+ }
556
+
557
+ private buildReferenceRegistry(schema: any): ReferenceRegistry {
558
+ const aliases = new Map<string, Record<string, any>>();
559
+ const positions: SchemaPosition[] = [];
560
+ const positionsByNode = new WeakMap<object, SchemaPosition>();
561
+
562
+ if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
563
+ return Object.freeze({
564
+ aliases: aliases as ReadonlyMap<string, Record<string, any>>,
565
+ positions: Object.freeze(positions),
566
+ positionsByNode
567
+ });
568
+ }
569
+
570
+ const register = (uri: string, node: Record<string, any>) => {
571
+ const existing = aliases.get(uri);
572
+ if (existing && existing !== node) {
573
+ const error = new ValidationError(`Duplicate schema identity: ${uri}`);
574
+ error.code = "DUPLICATE_SCHEMA_ID";
575
+ error.keyword = "$id";
576
+ throw error;
577
+ }
578
+ aliases.set(uri, node);
579
+ };
580
+ register(LOCAL_SCHEMA_BASE, schema);
581
+
582
+ const visited = new WeakSet<object>();
583
+ const stack: Array<{
584
+ node: Record<string, any>;
585
+ inheritedBase: string;
586
+ resourceRoot: Record<string, any>;
587
+ pointer: string;
588
+ }> = [
589
+ {
590
+ node: schema,
591
+ inheritedBase: LOCAL_SCHEMA_BASE,
592
+ resourceRoot: schema,
593
+ pointer: "#"
594
+ }
595
+ ];
596
+
597
+ while (stack.length > 0) {
598
+ const entry = stack.pop()!;
599
+ if (visited.has(entry.node)) {
600
+ continue;
601
+ }
602
+ visited.add(entry.node);
603
+
604
+ let baseUri = entry.inheritedBase;
605
+ let resourceRoot = entry.resourceRoot;
606
+ if (typeof entry.node.$id === "string" && !("$ref" in entry.node)) {
607
+ baseUri = this.resolveUri(entry.node.$id, entry.inheritedBase, "$id");
608
+ register(baseUri, entry.node);
609
+ if (baseUri.indexOf("#") === -1 || baseUri.endsWith("#")) {
610
+ resourceRoot = entry.node;
611
+ register(this.resourceUri(baseUri), entry.node);
612
+ }
613
+ }
614
+
615
+ const position = {
616
+ source: entry.node,
617
+ baseUri,
618
+ resourceRoot,
619
+ pointer: entry.pointer
620
+ };
621
+ positions.push(position);
622
+ positionsByNode.set(entry.node, position);
623
+
624
+ const children = this.registrySubschemaEntries(entry.node);
625
+ for (let index = children.length - 1; index >= 0; index--) {
626
+ const child = children[index];
627
+ if (Array.isArray(child.value)) {
628
+ continue;
629
+ }
630
+ stack.push({
631
+ node: child.value as Record<string, any>,
632
+ inheritedBase: baseUri,
633
+ resourceRoot,
634
+ pointer: `${entry.pointer}${child.pointer}`
635
+ });
636
+ }
637
+ }
638
+
639
+ return Object.freeze({
640
+ aliases: aliases as ReadonlyMap<string, Record<string, any>>,
641
+ positions: Object.freeze(positions),
642
+ positionsByNode
643
+ });
644
+ }
645
+
646
+ private resolveReferenceSource(
647
+ ref: string,
648
+ position: SchemaPosition,
649
+ registry: ReferenceRegistry
650
+ ): any {
651
+ const resolvedUri = this.resolveUri(ref, position.baseUri, "$ref");
652
+ const exactTarget = registry.aliases.get(resolvedUri);
653
+ if (exactTarget) {
654
+ return exactTarget;
655
+ }
656
+
657
+ const resourceRoot = registry.aliases.get(this.resourceUri(resolvedUri));
658
+ if (!resourceRoot) {
659
+ return;
660
+ }
661
+
662
+ const hashIndex = resolvedUri.indexOf("#");
663
+ const fragment = hashIndex === -1 ? "" : resolvedUri.slice(hashIndex + 1);
664
+ if (fragment.length === 0) {
665
+ return resourceRoot;
666
+ }
667
+ if (fragment.startsWith("/")) {
668
+ return resolvePath(resourceRoot, `#${fragment}`);
669
+ }
670
+
671
+ return;
672
+ }
673
+
674
+ private analyzeSchema(schema: any, registry: ReferenceRegistry): SchemaAnalysis {
675
+ if (!schema || typeof schema !== "object") {
676
+ return {
677
+ requiresDepthGuard: false,
678
+ requiresMutationJournal: false,
679
+ mutableSchemas: new WeakSet()
680
+ };
681
+ }
682
+
683
+ const visiting = new WeakSet<object>();
684
+ const visited = new WeakSet<object>();
685
+ const stack: Array<{
686
+ value: Record<string, any>;
687
+ depth: number;
688
+ exit: boolean;
689
+ }> = [{ value: schema, depth: 0, exit: false }];
690
+ let requiresDepthGuard = false;
691
+ let requiresMutationJournal = false;
692
+
693
+ while (stack.length > 0) {
694
+ const entry = stack.pop()!;
695
+ if (entry.exit) {
696
+ visiting.delete(entry.value);
697
+ visited.add(entry.value);
698
+ continue;
699
+ }
700
+ if (visited.has(entry.value)) {
701
+ continue;
702
+ }
703
+ if (visiting.has(entry.value)) {
704
+ const error = new ValidationError("Cyclic schema graph is not supported");
705
+ error.code = "CYCLIC_SCHEMA_GRAPH";
706
+ error.keyword = "compile";
707
+ throw error;
708
+ }
709
+ if (entry.depth > MAX_COMPILE_DEPTH) {
710
+ const error = new ValidationError("Maximum compile depth exceeded");
711
+ error.code = "MAX_COMPILE_DEPTH_EXCEEDED";
712
+ error.keyword = "compile";
713
+ throw error;
714
+ }
715
+ if (entry.depth > this.maxDepth) {
716
+ requiresDepthGuard = true;
717
+ }
718
+
719
+ visiting.add(entry.value);
720
+ stack.push({ ...entry, exit: true });
721
+
722
+ if (this.useDefaults !== false && this.hasPropertyDefaults(entry.value)) {
723
+ requiresMutationJournal = true;
724
+ }
725
+
726
+ for (const key of Object.keys(entry.value)) {
727
+ const keyword = this.getKeyword(key);
728
+ if (keyword && keyword !== keywords[key]) {
729
+ requiresDepthGuard = true;
730
+ requiresMutationJournal = true;
731
+ }
732
+ }
733
+
734
+ const children = this.schemaChildren(entry.value);
735
+ for (let index = children.length - 1; index >= 0; index--) {
736
+ stack.push({
737
+ value: children[index] as Record<string, any>,
738
+ depth: entry.depth + 1,
739
+ exit: false
740
+ });
741
+ }
742
+ }
743
+
744
+ const semanticState = new WeakMap<object, 1 | 2>();
745
+ const semanticStack: Array<{
746
+ value: Record<string, any>;
747
+ exit: boolean;
748
+ }> = [{ value: schema, exit: false }];
749
+ while (semanticStack.length > 0 && !requiresDepthGuard) {
750
+ const entry = semanticStack.pop()!;
751
+ if (entry.exit) {
752
+ semanticState.set(entry.value, 2);
753
+ continue;
754
+ }
755
+ const state = semanticState.get(entry.value);
756
+ if (state === 1) {
757
+ requiresDepthGuard = true;
758
+ break;
759
+ }
760
+ if (state === 2) {
761
+ continue;
762
+ }
763
+ semanticState.set(entry.value, 1);
764
+ semanticStack.push({ value: entry.value, exit: true });
765
+ const children = this.schemaChildren(entry.value);
766
+ if (
767
+ typeof entry.value.$ref === "string" &&
768
+ this.getKeyword("$ref") === keywords.$ref
769
+ ) {
770
+ const position = registry.positionsByNode.get(entry.value);
771
+ const target = position
772
+ ? this.resolveReferenceSource(entry.value.$ref, position, registry)
773
+ : undefined;
774
+ if (target && typeof target === "object") {
775
+ children.push(target);
776
+ }
777
+ }
778
+ for (let index = children.length - 1; index >= 0; index--) {
779
+ semanticStack.push({
780
+ value: children[index] as Record<string, any>,
781
+ exit: false
782
+ });
783
+ }
784
+ }
785
+
786
+ const mutableSchemas = new WeakSet<object>();
787
+ if (requiresMutationJournal) {
788
+ const mutationStack: Array<{
789
+ value: Record<string, any>;
790
+ exit: boolean;
791
+ }> = [{ value: schema, exit: false }];
792
+ const mutationVisited = new WeakSet<object>();
793
+ while (mutationStack.length > 0) {
794
+ const entry = mutationStack.pop()!;
795
+ if (entry.exit) {
796
+ const children = this.schemaChildren(entry.value);
797
+ const hasCustomKeyword = Object.keys(entry.value).some((key) => {
798
+ const keyword = this.getKeyword(key);
799
+ return !!keyword && keyword !== keywords[key];
800
+ });
801
+ if (
802
+ (this.useDefaults !== false &&
803
+ this.hasPropertyDefaults(entry.value)) ||
804
+ hasCustomKeyword ||
805
+ typeof entry.value.$ref === "string" ||
806
+ children.some((child) => mutableSchemas.has(child))
807
+ ) {
808
+ mutableSchemas.add(entry.value);
809
+ }
810
+ continue;
811
+ }
812
+ if (mutationVisited.has(entry.value)) {
813
+ continue;
814
+ }
815
+ mutationVisited.add(entry.value);
816
+ mutationStack.push({ value: entry.value, exit: true });
817
+ const children = this.schemaChildren(entry.value);
818
+ for (let index = children.length - 1; index >= 0; index--) {
819
+ mutationStack.push({
820
+ value: children[index] as Record<string, any>,
821
+ exit: false
822
+ });
823
+ }
824
+ }
825
+ }
826
+
827
+ return { requiresDepthGuard, requiresMutationJournal, mutableSchemas };
142
828
  }
143
829
 
144
830
  compile(schema: any): Validator {
145
- this.idRegistry.clear();
831
+ const prepared = this.prepareSchema(schema);
832
+ const compiledSchema = prepared.compiledSchema;
833
+ if (!prepared.requiresDepthGuard && !prepared.requiresMutationJournal) {
834
+ const directValidate = compiledSchema.$validate!;
835
+ const validate = (this.immutable
836
+ ? ((data: any) => {
837
+ const clonedData = deepCloneUnfreeze(data);
838
+ const error = directValidate(clonedData);
839
+ return error
840
+ ? { data: clonedData, error, valid: false }
841
+ : { data: clonedData, error: null, valid: true };
842
+ })
843
+ : ((data: any) => {
844
+ const error = directValidate(data);
845
+ return error
846
+ ? { data, error, valid: false }
847
+ : { data, error: null, valid: true };
848
+ })) as Validator;
849
+ validate.compiledSchema = compiledSchema;
850
+ return validate;
851
+ }
852
+ return this.createGuardedValidator(compiledSchema, prepared.depthGuardState!);
853
+ }
854
+
855
+ private prepareSchema(schema: any) {
856
+ const referenceRegistry = this.buildReferenceRegistry(schema);
857
+ const analysis = this.analyzeSchema(schema, referenceRegistry);
858
+ this.compileCache = new WeakMap();
859
+ this.compilingRequiresContext =
860
+ analysis.requiresDepthGuard || analysis.requiresMutationJournal;
861
+ this.compilingMutableSchemas = analysis.mutableSchemas;
146
862
  const compiledSchema = this.compileSchema(schema);
147
863
  this.rootSchema = compiledSchema;
864
+
865
+ let depthGuardState: DepthGuardState | null = null;
866
+ if (analysis.requiresDepthGuard) {
867
+ depthGuardState = this.installDepthGuards(compiledSchema);
868
+ definePropertyOrThrow(compiledSchema, "_requiresDepthGuard", {
869
+ value: true,
870
+ enumerable: false,
871
+ configurable: false,
872
+ writable: false
873
+ });
874
+ } else if (analysis.requiresMutationJournal) {
875
+ depthGuardState = { context: null };
876
+ }
877
+
148
878
  if ((compiledSchema as any)._hasRef === true) {
149
- this.linkReferences(compiledSchema);
879
+ this.linkReferences(referenceRegistry);
150
880
  }
151
881
 
152
882
  if (!compiledSchema.$validate) {
@@ -176,19 +906,62 @@ export class SchemaShield {
176
906
  }
177
907
  }
178
908
 
179
- const validate: Validator = (data: any) => {
180
- this.rootSchema = compiledSchema;
181
-
182
- const clonedData = this.immutable ? deepCloneUnfreeze(data) : data;
183
- const res = compiledSchema.$validate!(clonedData);
184
-
185
- if (res) {
186
- return { data: clonedData, error: res, valid: false };
187
- }
188
-
189
- return { data: clonedData, error: null, valid: true };
909
+ return {
910
+ compiledSchema,
911
+ requiresDepthGuard: analysis.requiresDepthGuard,
912
+ requiresMutationJournal: analysis.requiresMutationJournal,
913
+ depthGuardState
190
914
  };
915
+ }
191
916
 
917
+ private createGuardedValidator(
918
+ compiledSchema: CompiledSchema,
919
+ depthGuardState: DepthGuardState
920
+ ): Validator {
921
+ const reusableContext: ValidationContext = {
922
+ active: false,
923
+ depth: -1,
924
+ depthExceeded: false,
925
+ defaults: []
926
+ };
927
+ const validate = ((data: any) => {
928
+ this.rootSchema = compiledSchema;
929
+ const context = reusableContext.active
930
+ ? {
931
+ active: false,
932
+ depth: -1,
933
+ depthExceeded: false,
934
+ defaults: []
935
+ }
936
+ : reusableContext;
937
+ context.active = true;
938
+ context.depth = -1;
939
+ context.depthExceeded = false;
940
+ delete context.depthError;
941
+ context.defaults.length = 0;
942
+ this.validationContexts.push(context);
943
+ const priorContext = depthGuardState.context;
944
+ depthGuardState.context = context;
945
+ let clonedData = data;
946
+ try {
947
+ clonedData = this.immutable ? deepCloneUnfreeze(data) : data;
948
+ let error = compiledSchema.$validate!(clonedData);
949
+ if (this.isDepthError(error)) {
950
+ this.rollbackDefaults(context, 0);
951
+ error = context.depthError || this.depthError();
952
+ }
953
+ return error
954
+ ? { data: clonedData, error, valid: false }
955
+ : { data: clonedData, error: null, valid: true };
956
+ } catch (error) {
957
+ this.rollbackDefaults(context, 0);
958
+ throw error;
959
+ } finally {
960
+ depthGuardState.context = priorContext;
961
+ this.validationContexts.pop();
962
+ context.active = false;
963
+ }
964
+ }) as Validator;
192
965
  validate.compiledSchema = compiledSchema;
193
966
  return validate;
194
967
  }
@@ -346,7 +1119,7 @@ export class SchemaShield {
346
1119
  return;
347
1120
  }
348
1121
 
349
- Object.defineProperty(schema, "_hasRef", {
1122
+ definePropertyOrThrow(schema, "_hasRef", {
350
1123
  value: true,
351
1124
  enumerable: false,
352
1125
  configurable: false,
@@ -421,16 +1194,19 @@ export class SchemaShield {
421
1194
  }
422
1195
  }
423
1196
 
424
- private hasRequiredDefaults(schema: Record<string, any>): boolean {
1197
+ private hasPropertyDefaults(schema: Record<string, any>): boolean {
425
1198
  const properties = schema.properties;
426
1199
  if (!this.isPlainObject(properties)) {
427
1200
  return false;
428
1201
  }
429
1202
 
430
- const keys = Object.keys(properties);
431
- for (let i = 0; i < keys.length; i++) {
432
- const subSchema = properties[keys[i]];
433
- if (this.isPlainObject(subSchema) && "default" in subSchema) {
1203
+ const propertyKeys = Object.keys(properties);
1204
+ for (let i = 0; i < propertyKeys.length; i++) {
1205
+ const subSchema = properties[propertyKeys[i]];
1206
+ if (
1207
+ this.isPlainObject(subSchema) &&
1208
+ hasOwn(subSchema, "default")
1209
+ ) {
434
1210
  return true;
435
1211
  }
436
1212
  }
@@ -442,28 +1218,150 @@ export class SchemaShield {
442
1218
  return (Types as Record<string, TypeFunction | false>)[type] === validator;
443
1219
  }
444
1220
 
445
- private compileSchema(schema: Partial<CompiledSchema> | any): CompiledSchema {
446
- if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
447
- if (schema === true) {
448
- schema = { anyOf: [{}] }; // Always valid
449
- } else if (schema === false) {
450
- schema = { oneOf: [] }; // Always invalid
1221
+ private rollbackDefaults(context: ValidationContext, start: number) {
1222
+ for (let index = context.defaults.length - 1; index >= start; index--) {
1223
+ const entry = context.defaults[index];
1224
+ if (entry.descriptor) {
1225
+ definePropertyOrThrow(entry.target, entry.key, entry.descriptor);
451
1226
  } else {
452
- schema = { oneOf: [schema] };
1227
+ delete entry.target[entry.key];
453
1228
  }
454
1229
  }
1230
+ context.defaults.length = start;
1231
+ }
1232
+
1233
+ private isDepthError(error: Result): boolean {
1234
+ const context = this.validationContexts[this.validationContexts.length - 1];
1235
+ if (context?.depthExceeded) {
1236
+ return true;
1237
+ }
1238
+ if (!(error instanceof ValidationError)) {
1239
+ return false;
1240
+ }
1241
+ return error.getCause().code === "MAX_DEPTH_EXCEEDED";
1242
+ }
1243
+
1244
+ private validateSubschema(schema: CompiledSchema, data: any): Result {
1245
+ if (!schema || typeof schema.$validate !== "function") {
1246
+ return;
1247
+ }
1248
+ const context = this.validationContexts[this.validationContexts.length - 1];
1249
+ const savepoint = context?.defaults.length || 0;
1250
+ try {
1251
+ const error = schema.$validate(data);
1252
+ if (error && context) {
1253
+ this.rollbackDefaults(context, savepoint);
1254
+ }
1255
+ return error;
1256
+ } catch (error) {
1257
+ if (context) {
1258
+ this.rollbackDefaults(context, savepoint);
1259
+ }
1260
+ throw error;
1261
+ }
1262
+ }
1263
+
1264
+ private installDepthGuards(root: CompiledSchema): DepthGuardState {
1265
+ const state: DepthGuardState = { context: null };
1266
+ const stack: CompiledSchema[] = [root];
1267
+ const seen = new WeakSet<object>();
1268
+
1269
+ while (stack.length > 0) {
1270
+ const schema = stack.pop()!;
1271
+ if (!schema || typeof schema !== "object" || seen.has(schema)) {
1272
+ continue;
1273
+ }
1274
+ seen.add(schema);
1275
+
1276
+ if (typeof schema.$validate === "function") {
1277
+ const directValidate = schema.$validate;
1278
+ schema.$validate = getNamedFunction(directValidate.name, (data: any) => {
1279
+ const context = state.context;
1280
+ if (!context) {
1281
+ return directValidate(data);
1282
+ }
1283
+ const nextDepth = context.depth + 1;
1284
+ if (nextDepth > this.maxDepth) {
1285
+ context.depthExceeded = true;
1286
+ if (!context.depthError) {
1287
+ context.depthError = this.depthError();
1288
+ }
1289
+ return context.depthError;
1290
+ }
1291
+ context.depth = nextDepth;
1292
+ try {
1293
+ return directValidate(data);
1294
+ } finally {
1295
+ context.depth--;
1296
+ }
1297
+ });
1298
+ }
1299
+
1300
+ const children = this.schemaChildren(schema);
1301
+ for (const child of children) {
1302
+ stack.push(child as CompiledSchema);
1303
+ }
1304
+ }
1305
+ return state;
1306
+ }
1307
+
1308
+ private compileSchema(schema: Partial<CompiledSchema> | any): CompiledSchema {
1309
+ if (schema === true) {
1310
+ return {
1311
+ $validate: getNamedFunction<ValidateFunction>("Validate_True", () => {})
1312
+ };
1313
+ }
1314
+ if (schema === false) {
1315
+ const compiledFalse: CompiledSchema = {};
1316
+ const defineError = getDefinedErrorFunctionForKey(
1317
+ "oneOf",
1318
+ compiledFalse,
1319
+ this.failFast
1320
+ );
1321
+ compiledFalse.$validate = getNamedFunction<ValidateFunction>(
1322
+ "Validate_False",
1323
+ (data) => defineError("Value is not valid", { data })
1324
+ );
1325
+ return compiledFalse;
1326
+ }
1327
+ const sourceSchema =
1328
+ schema && typeof schema === "object" && !Array.isArray(schema)
1329
+ ? schema
1330
+ : null;
1331
+ const schemaCanApplyDefaults =
1332
+ sourceSchema !== null && this.compilingMutableSchemas.has(sourceSchema);
1333
+ if (sourceSchema) {
1334
+ const cached = this.compileCache.get(sourceSchema);
1335
+ if (cached) {
1336
+ return cached;
1337
+ }
1338
+ }
1339
+ if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
1340
+ schema = { oneOf: [schema] };
1341
+ }
455
1342
 
456
1343
  schema = this.normalizeSchemaForCompile(schema);
457
1344
 
458
1345
  const compiledSchema: CompiledSchema = deepCloneUnfreeze(
459
1346
  schema
460
1347
  ) as CompiledSchema;
1348
+ if (sourceSchema) {
1349
+ this.compileCache.set(sourceSchema, compiledSchema);
1350
+ }
1351
+ if (schemaCanApplyDefaults) {
1352
+ definePropertyOrThrow(compiledSchema, "_canApplyDefaults", {
1353
+ value: true,
1354
+ enumerable: false,
1355
+ configurable: false,
1356
+ writable: false
1357
+ });
1358
+ }
461
1359
 
462
- let schemaHasRef = false;
1360
+ const validateSubschema = this.compilingRequiresContext
1361
+ ? this.validateSubschema.bind(this)
1362
+ : undefined;
463
1363
 
464
- if (typeof schema.$id === "string") {
465
- this.idRegistry.set(schema.$id, compiledSchema);
466
- }
1364
+ let schemaHasRef = false;
467
1365
 
468
1366
  if ("$ref" in schema) {
469
1367
  schemaHasRef = true;
@@ -475,24 +1373,64 @@ export class SchemaShield {
475
1373
  this.failFast
476
1374
  );
477
1375
 
1376
+ const isBuiltinRef = refValidator === keywords.$ref;
478
1377
  compiledSchema.$validate = getNamedFunction<ValidateFunction>(
479
- "Validate_Reference",
1378
+ isBuiltinRef ? "Validate_Reference" : refValidator.name || "$ref",
480
1379
  (data) =>
481
1380
  (refValidator as KeywordFunction)(
482
1381
  compiledSchema,
483
1382
  data,
484
1383
  defineError,
485
- this
1384
+ this,
1385
+ validateSubschema
486
1386
  )
487
1387
  );
1388
+ if (!isBuiltinRef) {
1389
+ schemaHasRef = false;
1390
+ }
488
1391
  }
489
1392
 
490
- this.markSchemaHasRef(compiledSchema);
1393
+ for (const key of ["definitions", "$defs"]) {
1394
+ const definitions = schema[key];
1395
+ if (!definitions || typeof definitions !== "object") {
1396
+ continue;
1397
+ }
1398
+ const compiledDefinitions: Record<string, any> = {};
1399
+ for (const definitionKey of Object.keys(definitions)) {
1400
+ compiledDefinitions[definitionKey] = this.compileSchema(
1401
+ definitions[definitionKey]
1402
+ );
1403
+ }
1404
+ compiledSchema[key] = compiledDefinitions;
1405
+ }
1406
+ if (schemaHasRef) {
1407
+ this.markSchemaHasRef(compiledSchema);
1408
+ }
491
1409
  return compiledSchema;
492
1410
  }
493
1411
 
494
1412
  const validators: ValidatorItem[] = [];
495
1413
  const activeNames: string[] = [];
1414
+ const pendingCombinators: PendingCombinator[] = [];
1415
+
1416
+ if (
1417
+ this.useDefaults !== false &&
1418
+ this.getKeyword("properties") === keywords.properties &&
1419
+ this.hasPropertyDefaults(schema)
1420
+ ) {
1421
+ const applyDefaults =
1422
+ this.useDefaults === "empty"
1423
+ ? applyEmptyPropertyDefaults
1424
+ : applyPropertyDefaults;
1425
+ validators.push({
1426
+ name: applyDefaults.name,
1427
+ validate: getNamedFunction<ValidateFunction>(
1428
+ applyDefaults.name,
1429
+ (data) => applyDefaults(compiledSchema, data, this)
1430
+ )
1431
+ });
1432
+ activeNames.push(applyDefaults.name);
1433
+ }
496
1434
 
497
1435
  if ("type" in schema) {
498
1436
  const defineTypeError = getDefinedErrorFunctionForKey(
@@ -536,66 +1474,14 @@ export class SchemaShield {
536
1474
  if (typeFunctions.length === 1 && allTypesDefault) {
537
1475
  const singleTypeName = defaultTypeNames[0];
538
1476
  typeMethodName = singleTypeName;
539
-
540
- switch (singleTypeName) {
541
- case "object":
542
- combinedTypeValidator = (data) => {
543
- if (data === null || typeof data !== "object" || Array.isArray(data)) {
544
- return defineTypeError("Invalid type", { data });
545
- }
546
- };
547
- break;
548
- case "array":
549
- combinedTypeValidator = (data) => {
550
- if (!Array.isArray(data)) {
551
- return defineTypeError("Invalid type", { data });
552
- }
553
- };
554
- break;
555
- case "string":
556
- combinedTypeValidator = (data) => {
557
- if (typeof data !== "string") {
558
- return defineTypeError("Invalid type", { data });
559
- }
560
- };
561
- break;
562
- case "number":
563
- combinedTypeValidator = (data) => {
564
- if (typeof data !== "number") {
565
- return defineTypeError("Invalid type", { data });
566
- }
567
- };
568
- break;
569
- case "integer":
570
- combinedTypeValidator = (data) => {
571
- if (typeof data !== "number" || !Number.isInteger(data)) {
572
- return defineTypeError("Invalid type", { data });
573
- }
574
- };
575
- break;
576
- case "boolean":
577
- combinedTypeValidator = (data) => {
578
- if (typeof data !== "boolean") {
579
- return defineTypeError("Invalid type", { data });
580
- }
581
- };
582
- break;
583
- case "null":
584
- combinedTypeValidator = (data) => {
585
- if (data !== null) {
586
- return defineTypeError("Invalid type", { data });
587
- }
588
- };
589
- break;
590
- default: {
591
- const singleTypeFn = typeFunctions[0];
592
- combinedTypeValidator = (data) => {
593
- if (!singleTypeFn(data)) {
594
- return defineTypeError("Invalid type", { data });
595
- }
596
- };
597
- }
598
- }
1477
+ combinedTypeValidator =
1478
+ this.failFast && FAIL_FAST_TYPE_VALIDATORS[singleTypeName]
1479
+ ? FAIL_FAST_TYPE_VALIDATORS[singleTypeName]
1480
+ : createBuiltinTypeValidator(
1481
+ singleTypeName,
1482
+ defineTypeError,
1483
+ typeFunctions[0]
1484
+ );
599
1485
  } else if (typeFunctions.length > 1 && allTypesDefault) {
600
1486
  typeMethodName = defaultTypeNames.join("_OR_");
601
1487
 
@@ -611,6 +1497,9 @@ export class SchemaShield {
611
1497
  const dataType = typeof data;
612
1498
 
613
1499
  if (dataType === "number") {
1500
+ if (!Number.isFinite(data)) {
1501
+ return defineTypeError("Invalid type", { data });
1502
+ }
614
1503
  if (allowsNumber || (allowsInteger && Number.isInteger(data))) {
615
1504
  return;
616
1505
  }
@@ -689,13 +1578,8 @@ export class SchemaShield {
689
1578
 
690
1579
  const { type, $id, $ref, $validate, required, ...otherKeys } = schema; // Exclude handled keys
691
1580
 
692
- // In here we create an array of keys putting the require keyword last
693
- // This is to ensure required properties are checked after defaults are applied
694
- const keyOrder = required
695
- ? this.hasRequiredDefaults(schema)
696
- ? [...Object.keys(otherKeys), "required"]
697
- : ["required", ...Object.keys(otherKeys)]
698
- : Object.keys(otherKeys);
1581
+ const otherKeyNames = Object.keys(otherKeys);
1582
+ const keyOrder = required ? ["required", ...otherKeyNames] : otherKeyNames;
699
1583
 
700
1584
  for (const key of keyOrder) {
701
1585
  const keywordFn = this.getKeyword(key);
@@ -714,12 +1598,41 @@ export class SchemaShield {
714
1598
  this.failFast
715
1599
  );
716
1600
  const fnName = keywordFn.name || key;
1601
+ if (
1602
+ (key === "allOf" || key === "anyOf" || key === "oneOf") &&
1603
+ keywordFn === keywords[key]
1604
+ ) {
1605
+ const item: ValidatorItem = {
1606
+ name: fnName,
1607
+ validate: () => {
1608
+ throw new ValidationError("Combinator validator was not prepared");
1609
+ }
1610
+ };
1611
+ validators.push(item);
1612
+ pendingCombinators.push({ item, key, defineError });
1613
+ activeNames.push(fnName);
1614
+ continue;
1615
+ }
717
1616
 
1617
+ const keywordValidate = validateSubschema
1618
+ ? (data: any) =>
1619
+ (keywordFn as KeywordFunction)(
1620
+ compiledSchema,
1621
+ data,
1622
+ defineError,
1623
+ this,
1624
+ validateSubschema
1625
+ )
1626
+ : (data: any) =>
1627
+ (keywordFn as KeywordFunction)(
1628
+ compiledSchema,
1629
+ data,
1630
+ defineError,
1631
+ this
1632
+ );
718
1633
  validators.push({
719
1634
  name: fnName,
720
- validate: getNamedFunction<ValidateFunction>(fnName, (data) =>
721
- (keywordFn as KeywordFunction)(compiledSchema, data, defineError, this)
722
- )
1635
+ validate: getNamedFunction<ValidateFunction>(fnName, keywordValidate)
723
1636
  });
724
1637
 
725
1638
  activeNames.push(fnName);
@@ -774,6 +1687,69 @@ export class SchemaShield {
774
1687
  }
775
1688
  }
776
1689
 
1690
+ if (this.isPlainObject(schema.properties)) {
1691
+ definePropertyOrThrow(compiledSchema, "_propKeys", {
1692
+ value: Object.keys(schema.properties),
1693
+ enumerable: false,
1694
+ configurable: false,
1695
+ writable: false
1696
+ });
1697
+ }
1698
+
1699
+ if (
1700
+ this.useDefaults !== false &&
1701
+ this.isPlainObject(schema.properties) &&
1702
+ this.hasPropertyDefaults(schema)
1703
+ ) {
1704
+ const defaultKeys = Object.keys(schema.properties).filter(
1705
+ (key: string) => {
1706
+ const property = schema.properties[key];
1707
+ return (
1708
+ property &&
1709
+ typeof property === "object" &&
1710
+ !Array.isArray(property) &&
1711
+ hasOwn(property, "default")
1712
+ );
1713
+ }
1714
+ );
1715
+ if (defaultKeys.length > 0) {
1716
+ definePropertyOrThrow(compiledSchema, "_defaultKeys", {
1717
+ value: defaultKeys,
1718
+ enumerable: false,
1719
+ configurable: false,
1720
+ writable: false
1721
+ });
1722
+ }
1723
+ }
1724
+
1725
+ prepareCombinatorEntries(compiledSchema);
1726
+
1727
+ for (let index = 0; index < pendingCombinators.length; index++) {
1728
+ const pending = pendingCombinators[index];
1729
+ const transactions =
1730
+ (compiledSchema as any)._canApplyDefaults === true
1731
+ ? {
1732
+ savepoint: () => this.#defaultSavepoint(),
1733
+ rollback: (savepoint: number) =>
1734
+ this.#rollbackDefaultSavepoint(savepoint),
1735
+ capture: (savepoint: number) =>
1736
+ this.#captureDefaultSavepoint(savepoint),
1737
+ restore: (mutations: DefaultMutation[]) =>
1738
+ this.#restoreDefaults(mutations)
1739
+ }
1740
+ : undefined;
1741
+ pending.item.validate = getNamedFunction(
1742
+ pending.item.name,
1743
+ createCombinatorValidator(
1744
+ pending.key,
1745
+ compiledSchema,
1746
+ pending.defineError,
1747
+ validateSubschema,
1748
+ transactions
1749
+ )
1750
+ );
1751
+ }
1752
+
777
1753
  if (schemaHasRef) {
778
1754
  this.markSchemaHasRef(compiledSchema);
779
1755
  }
@@ -827,64 +1803,90 @@ export class SchemaShield {
827
1803
  return false;
828
1804
  }
829
1805
 
830
- private linkReferences(root: CompiledSchema) {
831
- const stack: any[] = [root];
1806
+ private getCompiledReferenceTarget(
1807
+ ref: string,
1808
+ position: SchemaPosition,
1809
+ registry: ReferenceRegistry
1810
+ ): CompiledSchema | boolean | undefined {
1811
+ const resolvedUri = this.resolveUri(ref, position.baseUri, "$ref");
1812
+ const exactTarget = registry.aliases.get(resolvedUri);
1813
+ if (exactTarget) {
1814
+ return this.compileCache.get(exactTarget);
1815
+ }
832
1816
 
833
- while (stack.length > 0) {
834
- const node = stack.pop();
1817
+ const resourceRoot = registry.aliases.get(this.resourceUri(resolvedUri));
1818
+ if (!resourceRoot) {
1819
+ return;
1820
+ }
1821
+ const compiledResource = this.compileCache.get(resourceRoot);
1822
+ if (!compiledResource) {
1823
+ return;
1824
+ }
835
1825
 
836
- if (!node || typeof node !== "object") continue;
1826
+ const hashIndex = resolvedUri.indexOf("#");
1827
+ const fragment = hashIndex === -1 ? "" : resolvedUri.slice(hashIndex + 1);
1828
+ if (fragment.length === 0) {
1829
+ return compiledResource;
1830
+ }
1831
+ if (fragment.startsWith("/")) {
1832
+ return resolvePath(compiledResource, `#${fragment}`);
1833
+ }
837
1834
 
1835
+ return;
1836
+ }
1837
+
1838
+ private linkReferences(registry: ReferenceRegistry) {
1839
+ for (let index = 0; index < registry.positions.length; index++) {
1840
+ const position = registry.positions[index];
838
1841
  if (
839
- typeof node.$ref === "string" &&
840
- typeof node.$validate === "function" &&
841
- node.$validate.name === "Validate_Reference"
1842
+ typeof position.source.$ref !== "string" ||
1843
+ this.getKeyword("$ref") !== keywords.$ref
842
1844
  ) {
843
- const refPath = node.$ref as string;
844
-
845
- let target: any = this.getSchemaRef(refPath);
846
- if (typeof target === "undefined") {
847
- target = this.getSchemaById(refPath);
848
- }
849
-
850
- if (typeof target === "boolean") {
851
- if (target === true) {
852
- node.$validate = getNamedFunction("Validate_Ref_True", () => {});
853
- } else {
854
- const defineError = getDefinedErrorFunctionForKey(
855
- "$ref",
856
- node as any,
857
- this.failFast
858
- );
859
-
860
- node.$validate = getNamedFunction(
861
- "Validate_Ref_False",
862
- (_data: any) => defineError("Value is not valid")
863
- );
864
- }
865
- continue;
866
- }
867
-
868
- if (target && typeof target.$validate === "function") {
869
- node.$validate = target.$validate;
870
- }
1845
+ continue;
871
1846
  }
872
1847
 
873
- for (const key in node) {
874
- const value = node[key];
875
- if (!value) continue;
1848
+ const node = this.compileCache.get(position.source);
1849
+ const target = this.getCompiledReferenceTarget(
1850
+ position.source.$ref,
1851
+ position,
1852
+ registry
1853
+ );
1854
+ if (!node || typeof target === "undefined") {
1855
+ const error = new ValidationError(
1856
+ `Reference not found: ${position.source.$ref}`
1857
+ );
1858
+ error.code = "REFERENCE_NOT_FOUND";
1859
+ error.keyword = "$ref";
1860
+ throw error;
1861
+ }
876
1862
 
877
- if (Array.isArray(value)) {
878
- for (let i = 0; i < value.length; i++) {
879
- const v = value[i];
880
- if (v && typeof v === "object") {
881
- stack.push(v);
882
- }
883
- }
884
- } else if (typeof value === "object") {
885
- stack.push(value);
1863
+ let targetValidate: ValidateFunction;
1864
+ if (target === true) {
1865
+ targetValidate = getNamedFunction("Validate_Ref_True", () => {});
1866
+ } else if (target === false) {
1867
+ const defineError = getDefinedErrorFunctionForKey(
1868
+ "$ref",
1869
+ node,
1870
+ this.failFast
1871
+ );
1872
+ targetValidate = getNamedFunction("Validate_Ref_False", (data: any) =>
1873
+ defineError("Value is not valid", { data })
1874
+ );
1875
+ } else {
1876
+ if (typeof target.$validate !== "function") {
1877
+ target.$validate = getNamedFunction<ValidateFunction>(
1878
+ "Validate_Ref_Any",
1879
+ () => {}
1880
+ );
886
1881
  }
1882
+ targetValidate = target.$validate;
887
1883
  }
1884
+ definePropertyOrThrow(node, "_resolvedRef", {
1885
+ value: targetValidate,
1886
+ enumerable: false,
1887
+ configurable: false,
1888
+ writable: false
1889
+ });
888
1890
  }
889
1891
  }
890
1892
  }