@jarenjs/validate 0.8.3 → 0.9.2
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/ARCHITECTURE.md +1067 -0
- package/LICENSE +21 -0
- package/README.md +339 -2
- package/dist/types/array.d.ts +2 -0
- package/dist/types/bigint.d.ts +1 -0
- package/dist/types/combine.d.ts +1 -0
- package/dist/types/condition.d.ts +1 -0
- package/dist/types/content.d.ts +3 -0
- package/dist/types/data.d.ts +7 -0
- package/dist/types/dollar-data.d.ts +20 -0
- package/dist/types/dynamic-ref.d.ts +44 -0
- package/dist/types/enum.d.ts +1 -0
- package/dist/types/format.d.ts +21 -0
- package/dist/types/index.d.ts +874 -0
- package/dist/types/number.d.ts +1 -0
- package/dist/types/object.d.ts +3 -0
- package/dist/types/query-keyword.d.ts +19 -0
- package/dist/types/query.d.ts +29 -0
- package/dist/types/schema.d.ts +1 -0
- package/dist/types/string.d.ts +1 -0
- package/dist/types/tools.d.ts +51 -0
- package/dist/types/traverse.d.ts +32 -0
- package/dist/types/unevaluated.d.ts +12 -0
- package/package.json +32 -7
- package/src/array.js +565 -0
- package/src/bigint.js +97 -0
- package/src/combine.js +226 -0
- package/src/condition.js +109 -0
- package/src/content.js +83 -0
- package/src/data.js +477 -0
- package/src/dollar-data.js +629 -0
- package/src/dynamic-ref.js +121 -0
- package/src/enum.js +148 -0
- package/src/format.js +66 -0
- package/src/index.js +1854 -0
- package/src/number.js +159 -0
- package/src/object.js +755 -0
- package/src/query-keyword.js +99 -0
- package/src/query.js +59 -0
- package/src/schema.js +645 -0
- package/src/string.js +152 -0
- package/src/tools.js +205 -0
- package/src/traverse.js +433 -0
- package/src/unevaluated.js +151 -0
- package/dist/index.js +0 -1802
- package/dist/index.js.map +0 -7
- package/dist/index.min.js +0 -2
- package/dist/index.min.js.map +0 -7
package/ARCHITECTURE.md
ADDED
|
@@ -0,0 +1,1067 @@
|
|
|
1
|
+
# @jarenjs/validate Architecture
|
|
2
|
+
|
|
3
|
+
**IMPORTANT**: Update this document only when introducing architectural changes. If multiple architects are involved, ensure consensus on significant changes.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
**@jarenjs/validate** is a high-performance JSON Schema validator for JavaScript/TypeScript. It implements a **compilation-based architecture** that transforms JSON Schemas into optimized validation functions at build time, rather than interpreting schemas during validation. This design choice delivers significant performance advantages over traditional interpretation-based validators.
|
|
8
|
+
|
|
9
|
+
### Core Design Philosophy
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
Schema → Compile → Optimized Validator Function → Execute against Data
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The validator embraces these architectural principles:
|
|
16
|
+
|
|
17
|
+
1. **Compile-Time Optimization**: Schemas are parsed and compiled once, generating specialized validation functions
|
|
18
|
+
2. **Fast Path Specialization**: Common schema patterns receive dedicated optimized code paths
|
|
19
|
+
3. **Zero-Copy Validation**: Minimal object allocations during validation through careful memory management
|
|
20
|
+
4. **Reference Pre-Resolution**: All `$ref` references are resolved and flattened at compile time
|
|
21
|
+
5. **Lazy Error Generation**: Error objects are only created when explicitly requested
|
|
22
|
+
|
|
23
|
+
### Key Files
|
|
24
|
+
|
|
25
|
+
| File | Purpose |
|
|
26
|
+
|------|---------|
|
|
27
|
+
| `packages/validate/src/index.js` | Main validator classes (`JarenValidator`, `ValidationRoot`, `ValidationObject`) |
|
|
28
|
+
| `packages/validate/src/traverse.js` | Schema traversal and ref resolution (`storeSchemaIdsInMap`, `restoreSchemaRefsInMap`) |
|
|
29
|
+
| `packages/validate/src/schema.js` | Schema compilation dispatcher (`compileSchemaObject`) |
|
|
30
|
+
| `packages/validate/src/array.js` | Array validation logic |
|
|
31
|
+
| `packages/validate/src/object.js` | Object validation logic |
|
|
32
|
+
| `packages/validate/src/string.js` | String validation logic |
|
|
33
|
+
| `packages/validate/src/number.js` | Number validation logic |
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Core Design Principles
|
|
38
|
+
|
|
39
|
+
### 1. Compile-Time Optimization
|
|
40
|
+
|
|
41
|
+
The most important architectural insight: **all ref resolution must happen at compile time**. This means:
|
|
42
|
+
|
|
43
|
+
- `$ref` chains are flattened during schema loading
|
|
44
|
+
- Validation functions are pre-compiled before any data is validated
|
|
45
|
+
- No URI resolution happens during validation
|
|
46
|
+
|
|
47
|
+
### 2. Lazy vs Eager Evaluation Trade-offs
|
|
48
|
+
|
|
49
|
+
Refs are resolved eagerly at compile time, never lazily on first validation. This keeps the first `validate(data)` call as fast as every subsequent one:
|
|
50
|
+
|
|
51
|
+
```javascript
|
|
52
|
+
const validate = compile(schema); // All refs resolved here
|
|
53
|
+
validate(data); // Direct function call, no resolution
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 3. Function Inlining
|
|
57
|
+
|
|
58
|
+
To reduce call stack depth, simple schemas compile to inline validation functions rather than delegating to helper functions:
|
|
59
|
+
|
|
60
|
+
```javascript
|
|
61
|
+
// Before: Multiple function calls
|
|
62
|
+
function validate(data) {
|
|
63
|
+
return validateType(data) && validateString(data);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// After: example, but use @jarenjs/core
|
|
67
|
+
function validate(data) {
|
|
68
|
+
if (typeof data !== 'string') return false;
|
|
69
|
+
if (data.length > maxLength) return false;
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Four-Phase Architecture
|
|
77
|
+
|
|
78
|
+
The validator operates in four distinct phases that cleanly separate concerns:
|
|
79
|
+
|
|
80
|
+
### Phase 1: Schema Registration
|
|
81
|
+
|
|
82
|
+
**Purpose**: Build a map of all reachable schemas by URI.
|
|
83
|
+
|
|
84
|
+
**Entry Point**: `JarenValidator.addSchema(schema, key)`
|
|
85
|
+
|
|
86
|
+
**Flow**:
|
|
87
|
+
```
|
|
88
|
+
addSchema(schema, key)
|
|
89
|
+
├── Store schema under key (and alt key with/without #)
|
|
90
|
+
└── #traverseAndStoreIds(baseUri, schema)
|
|
91
|
+
└── storeSchemaIdsInMap(schemasMap, baseUri, schema)
|
|
92
|
+
├── Store root schema under baseUri
|
|
93
|
+
└── BFS traverse schema structure:
|
|
94
|
+
├── On $id: Store subschema, update baseUri
|
|
95
|
+
├── On $anchor: Store anchor
|
|
96
|
+
├── On $ref: Store null placeholder (mark as ref)
|
|
97
|
+
└── On schema objects/arrays: Continue traversal
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Key Data Structure**: `#schemas Map<string, schema|null>`
|
|
101
|
+
|
|
102
|
+
- Keys are normalized URIs (e.g., `http://example.com/schema#`, `#/$defs/foo`)
|
|
103
|
+
- Values are either the schema object or `null` (for refs that point elsewhere)
|
|
104
|
+
- Stored at `JarenValidator` instance level (survives multiple `compile()` calls)
|
|
105
|
+
|
|
106
|
+
**Important**: At this phase, refs are stored as `null` placeholders. The actual resolution happens later.
|
|
107
|
+
|
|
108
|
+
### Phase 2: Schema Traversal
|
|
109
|
+
|
|
110
|
+
**Purpose**: Create a schemas map for a specific compilation, merging instance schemas with passed schemas.
|
|
111
|
+
|
|
112
|
+
**Entry Point**: `JarenValidator.compile(schema, schemas)`
|
|
113
|
+
|
|
114
|
+
**Flow**:
|
|
115
|
+
```
|
|
116
|
+
compile(schema, schemas)
|
|
117
|
+
└── #traverseSchema(schema, schemas, instanceSchemas)
|
|
118
|
+
├── Create new schemaMap from instanceSchemas
|
|
119
|
+
├── storeSchemaIdsInMap(schemaMap, origin, schema)
|
|
120
|
+
├── (Optional) storeSchemaIdsInMap for additional schemas
|
|
121
|
+
└── restoreSchemaRefsInMap(schemaMap)
|
|
122
|
+
└── For each null entry:
|
|
123
|
+
├── Try resolveRefSchemaDeep (flatten chains)
|
|
124
|
+
└── On failure: resolveRefSchemaShallow (single hop)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Key Data Structure**: `schemaMap Map<string, schema>` (local to this compilation)
|
|
128
|
+
|
|
129
|
+
- Merges instance-level schemas (`this.#schemas`) with compile-time schemas
|
|
130
|
+
- After `restoreSchemaRefsInMap`, all ref values are resolved schemas (not null)
|
|
131
|
+
- **Ref chains are flattened**: `A → B → C` becomes `A → C`
|
|
132
|
+
|
|
133
|
+
**The Critical Optimization**: `restoreSchemaRefsInMap` now uses `resolveRefSchemaDeep` instead of `resolveRefSchemaShallow`. This follows the entire ref chain at load time and stores the final schema directly.
|
|
134
|
+
|
|
135
|
+
### Phase 3: Compilation
|
|
136
|
+
|
|
137
|
+
**Purpose**: Create compiled validator functions from schemas.
|
|
138
|
+
|
|
139
|
+
**Entry Point**: `JarenValidator.#compileSchemaWithRoot(...)`
|
|
140
|
+
|
|
141
|
+
**Flow**:
|
|
142
|
+
```
|
|
143
|
+
compile()
|
|
144
|
+
├── Create ValidationRoot(origin, schemaMap, formats, options)
|
|
145
|
+
├── #precompileRefs(root, schemaMap, origin)
|
|
146
|
+
│ └── For each ref in schemaMap:
|
|
147
|
+
│ ├── Skip if already compiled
|
|
148
|
+
│ └── root.createObject(id, schema, origin)
|
|
149
|
+
│ └── ValidationObject(root, id, schema, baseUri)
|
|
150
|
+
│ └── compileValidator() → returns validator function
|
|
151
|
+
└── Return jarenValidateSchema(data) function
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Key Classes**:
|
|
155
|
+
|
|
156
|
+
#### ValidationRoot
|
|
157
|
+
- Manages the `Map<string, ValidationObject>` called `#objects`
|
|
158
|
+
- Entry point: `validate(data)` clears errors and calls `#firstSchema.validate(data, data)`
|
|
159
|
+
- Creates objects via `createObject()` and caches them in `#objects`
|
|
160
|
+
- **Pre-compilation**: Now creates ValidationObjects for ALL refs before returning
|
|
161
|
+
|
|
162
|
+
#### ValidationObject
|
|
163
|
+
- Represents a single schema location (identified by URI)
|
|
164
|
+
- **Constructor**: Immediately compiles validator via `compileValidator()`
|
|
165
|
+
- **Key property**: `#validator` - the compiled function that validates data
|
|
166
|
+
- For refs: `#validator` is the target validator function (direct reference)
|
|
167
|
+
|
|
168
|
+
**The Critical Optimization**: `#precompileRefs()` iterates through `schemaMap` and creates `ValidationObject` instances for every ref. This moves object creation from validation-time to compile-time.
|
|
169
|
+
|
|
170
|
+
### Phase 4: Validation
|
|
171
|
+
|
|
172
|
+
**Purpose**: Validate data against compiled schema.
|
|
173
|
+
|
|
174
|
+
**Entry Point**: `jarenValidateSchema(data)`
|
|
175
|
+
|
|
176
|
+
**Flow**:
|
|
177
|
+
```
|
|
178
|
+
jarenValidateSchema(data)
|
|
179
|
+
└── root.validate(data)
|
|
180
|
+
└── #firstSchema.validate(data, dataRoot)
|
|
181
|
+
└── this.#validator(data, dataRoot)
|
|
182
|
+
└── Either:
|
|
183
|
+
├── compileSchemaObject() result (for non-ref schemas)
|
|
184
|
+
└── Target validator function (for refs, pre-compiled)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**Key Insight**: For refs, `this.#validator` is **already** the target validator function (set during Phase 3 pre-compilation). No resolution happens at validation time.
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## High-Level Architecture
|
|
192
|
+
|
|
193
|
+
```mermaid
|
|
194
|
+
flowchart TB
|
|
195
|
+
subgraph "Public API Layer"
|
|
196
|
+
A[JarenValidator]
|
|
197
|
+
B[ValidationOptions]
|
|
198
|
+
C[ValidatorOptions]
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
subgraph "Compilation Layer"
|
|
202
|
+
D[ValidationRoot]
|
|
203
|
+
E[ValidationObject]
|
|
204
|
+
F[Schema Compilation]
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
subgraph "Schema Processing"
|
|
208
|
+
G[Schema Traversal]
|
|
209
|
+
H[Reference Resolution]
|
|
210
|
+
I[JSON Pointer Handling]
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
subgraph "Keyword Compilers"
|
|
214
|
+
J[Type Validators]
|
|
215
|
+
K[String Keywords]
|
|
216
|
+
L[Number Keywords]
|
|
217
|
+
M[Array Keywords]
|
|
218
|
+
N[Object Keywords]
|
|
219
|
+
O[Combine Keywords]
|
|
220
|
+
P[Conditional Keywords]
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
subgraph "Runtime Dependencies"
|
|
224
|
+
Q[@jarenjs/core]
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
A --> D
|
|
228
|
+
D --> E
|
|
229
|
+
E --> F
|
|
230
|
+
F --> J & K & L & M & N & O & P
|
|
231
|
+
G --> H
|
|
232
|
+
H --> I
|
|
233
|
+
D --> G
|
|
234
|
+
J & K & L & M & N & O & P --> Q
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Module Organization
|
|
240
|
+
|
|
241
|
+
```mermaid
|
|
242
|
+
flowchart LR
|
|
243
|
+
subgraph "Entry Point"
|
|
244
|
+
INDEX[index.js]
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
subgraph "Core Classes"
|
|
248
|
+
INDEX --> VR[ValidationRoot]
|
|
249
|
+
INDEX --> VO[ValidationObject]
|
|
250
|
+
INDEX --> JV[JarenValidator]
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
subgraph "Schema Compilation"
|
|
254
|
+
VO --> SCHEMA[schema.js]
|
|
255
|
+
SCHEMA --> TYPE[type validation]
|
|
256
|
+
SCHEMA --> COMPILE[compileSchemaObject]
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
subgraph "Keyword Modules"
|
|
260
|
+
SCHEMA --> STRING[string.js]
|
|
261
|
+
SCHEMA --> NUMBER[number.js]
|
|
262
|
+
SCHEMA --> BIGINT[bigint.js]
|
|
263
|
+
SCHEMA --> ARRAY[array.js]
|
|
264
|
+
SCHEMA --> OBJECT[object.js]
|
|
265
|
+
SCHEMA --> COMBINE[combine.js]
|
|
266
|
+
SCHEMA --> CONDITION[condition.js]
|
|
267
|
+
SCHEMA --> ENUM[enum.js]
|
|
268
|
+
SCHEMA --> FORMAT[format.js]
|
|
269
|
+
SCHEMA --> CONTENT[content.js]
|
|
270
|
+
SCHEMA --> DATA[data.js]
|
|
271
|
+
SCHEMA --> DDLR[dollar-data.js]
|
|
272
|
+
SCHEMA --> UNEVAL[unevaluated.js]
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
subgraph "Infrastructure"
|
|
276
|
+
INDEX --> TRAVERSE[traverse.js]
|
|
277
|
+
INDEX --> TOOLS[tools.js]
|
|
278
|
+
INDEX --> DYNREF[dynamic-ref.js]
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
subgraph "External Dependencies"
|
|
282
|
+
TOOLS --> CORE[@jarenjs/core]
|
|
283
|
+
end
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## Core Classes
|
|
289
|
+
|
|
290
|
+
### JarenValidator
|
|
291
|
+
|
|
292
|
+
The primary entry point for consumers. Manages schema registration, format registration, and orchestrates the compilation process.
|
|
293
|
+
|
|
294
|
+
```mermaid
|
|
295
|
+
classDiagram
|
|
296
|
+
class JarenValidator {
|
|
297
|
+
-Map #schemas
|
|
298
|
+
-Map #metaSchemas
|
|
299
|
+
-object #formats
|
|
300
|
+
-ValidatorOptions #options
|
|
301
|
+
+addSchema(schema, key)
|
|
302
|
+
+addFormat(name, compiler)
|
|
303
|
+
+addFormats(compilers)
|
|
304
|
+
+compile(schema)
|
|
305
|
+
+validateSchema(schema)
|
|
306
|
+
+addMetaSchema(schema, key)
|
|
307
|
+
+getSchema(key)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
class ValidationRoot {
|
|
311
|
+
-string #rootOrigin
|
|
312
|
+
-Map #schemas
|
|
313
|
+
-object #formats
|
|
314
|
+
-ValidationOptions #options
|
|
315
|
+
-TraverseOptions #traverse
|
|
316
|
+
-Map #objects
|
|
317
|
+
-Array #errors
|
|
318
|
+
-ValidationObject #firstSchema
|
|
319
|
+
+validate(data)
|
|
320
|
+
+createObject(path, schema, baseUri)
|
|
321
|
+
+resolveObject(ref, path, schema)
|
|
322
|
+
+addError(error)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
class ValidationObject {
|
|
326
|
+
-ValidationRoot #root
|
|
327
|
+
-string #path
|
|
328
|
+
-Array #members
|
|
329
|
+
-object #schema
|
|
330
|
+
-function #validator
|
|
331
|
+
-string #baseUri
|
|
332
|
+
-string #effectiveBaseUri
|
|
333
|
+
+createValidator(schema, key, index)
|
|
334
|
+
+createErrorHandler(expected, key)
|
|
335
|
+
+validate(data, dataPath, dataRoot)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
JarenValidator --> ValidationRoot : creates
|
|
339
|
+
ValidationRoot --> ValidationObject : manages
|
|
340
|
+
ValidationObject --> ValidationObject : creates children
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### ValidationRoot
|
|
344
|
+
|
|
345
|
+
The compilation context that:
|
|
346
|
+
- Maintains the schema registry (URI → Schema mapping)
|
|
347
|
+
- Manages format validators
|
|
348
|
+
- Houses all compiled ValidationObjects
|
|
349
|
+
- Collects validation errors
|
|
350
|
+
- Provides `$ref` resolution services
|
|
351
|
+
|
|
352
|
+
### ValidationObject
|
|
353
|
+
|
|
354
|
+
Represents a single schema location with its compiled validator:
|
|
355
|
+
- **Identity**: URI path identifying this schema object
|
|
356
|
+
- **State**: The schema definition and compiled validator function
|
|
357
|
+
- **Relationships**: Parent root reference, child members
|
|
358
|
+
- **Behavior**: Creates child validators, handles error generation
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
## Compilation Pipeline
|
|
363
|
+
|
|
364
|
+
```mermaid
|
|
365
|
+
sequenceDiagram
|
|
366
|
+
participant User
|
|
367
|
+
participant JV as JarenValidator
|
|
368
|
+
participant VR as ValidationRoot
|
|
369
|
+
participant VO as ValidationObject
|
|
370
|
+
participant SC as Schema Compilers
|
|
371
|
+
participant TD as Type-specific Compilers
|
|
372
|
+
|
|
373
|
+
User->>JV: compile(schema)
|
|
374
|
+
JV->>JV: #traverseSchema()
|
|
375
|
+
Note over JV: Build schema map with<br/>all $id and $ref locations
|
|
376
|
+
|
|
377
|
+
JV->>VR: new ValidationRoot(origin, schemas, formats, opts)
|
|
378
|
+
VR->>VR: Pre-compile all refs
|
|
379
|
+
|
|
380
|
+
VR->>VO: create root ValidationObject
|
|
381
|
+
VO->>VO: compileValidator()
|
|
382
|
+
|
|
383
|
+
alt Has $ref
|
|
384
|
+
VO->>VR: resolveObject(ref)
|
|
385
|
+
VR-->>VO: cached ValidationObject
|
|
386
|
+
else Complex Schema
|
|
387
|
+
VO->>SC: compileSchemaObject()
|
|
388
|
+
SC->>TD: compile keywords
|
|
389
|
+
TD-->>SC: validators[]
|
|
390
|
+
SC->>SC: compose validators
|
|
391
|
+
SC-->>VO: compiled function
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
VO-->>VR: ready
|
|
395
|
+
VR-->>JV: ValidationRoot ready
|
|
396
|
+
JV-->>User: validation function
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
---
|
|
400
|
+
|
|
401
|
+
## Schema Compilation Flow
|
|
402
|
+
|
|
403
|
+
```mermaid
|
|
404
|
+
flowchart TD
|
|
405
|
+
A[JSON Schema Input] --> B{Schema Type}
|
|
406
|
+
|
|
407
|
+
B -->|true| C[Return trueThat]
|
|
408
|
+
B -->|false| D[Return false validator]
|
|
409
|
+
B -->|object| E{Has $ref?}
|
|
410
|
+
|
|
411
|
+
E -->|Yes| F[Delegate to ref resolver]
|
|
412
|
+
E -->|No| G{Fast Path?}
|
|
413
|
+
|
|
414
|
+
G -->|Type only| H[compileTypeSimple]
|
|
415
|
+
G -->|Required only| I[compileRequired]
|
|
416
|
+
G -->|MinLength only| J[compileMinLengthFast]
|
|
417
|
+
G -->|Complex| K[Full Compilation]
|
|
418
|
+
|
|
419
|
+
K --> L[compileTypeBasic]
|
|
420
|
+
K --> M[compileEnumBasic]
|
|
421
|
+
K --> N[compileDollarDataSchema]
|
|
422
|
+
K --> O[compileNumberBasic]
|
|
423
|
+
K --> P[compileBigIntBasic]
|
|
424
|
+
K --> Q[compileStringBasic]
|
|
425
|
+
K --> R[compileFormatBasic]
|
|
426
|
+
K --> S[compileContentSchema]
|
|
427
|
+
K --> T[compileArraySchema]
|
|
428
|
+
K --> U[compileObjectSchema]
|
|
429
|
+
K --> V[compileCombineSchema]
|
|
430
|
+
K --> W[compileConditionSchema]
|
|
431
|
+
|
|
432
|
+
L & M & N & O & P & Q & R & S & T & U & V & W --> X[Compose validators]
|
|
433
|
+
X --> Y{Count}
|
|
434
|
+
|
|
435
|
+
Y -->|0| C
|
|
436
|
+
Y -->|1| Z[Return single]
|
|
437
|
+
Y -->|2| AA[Dual validator]
|
|
438
|
+
Y -->|3| AB[Triple validator]
|
|
439
|
+
Y -->|4+| AC[Loop validator]
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
---
|
|
443
|
+
|
|
444
|
+
## Keyword Compiler Architecture
|
|
445
|
+
|
|
446
|
+
Each keyword module follows a consistent pattern:
|
|
447
|
+
|
|
448
|
+
```mermaid
|
|
449
|
+
flowchart LR
|
|
450
|
+
A[Schema Object] --> B[Extract Keyword Value]
|
|
451
|
+
B --> C{Value Valid?}
|
|
452
|
+
C -->|No| D[Return undefined]
|
|
453
|
+
C -->|Yes| E[Create Error Handler]
|
|
454
|
+
E --> F[Compile Validator Function]
|
|
455
|
+
F --> G[Return Closure]
|
|
456
|
+
|
|
457
|
+
style D fill:#f99
|
|
458
|
+
style G fill:#9f9
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
### Example: String Keyword Compilation
|
|
462
|
+
|
|
463
|
+
```mermaid
|
|
464
|
+
flowchart TD
|
|
465
|
+
A[compileStringBasic] --> B[compileStringIntern]
|
|
466
|
+
|
|
467
|
+
B --> C[compileMinLength]
|
|
468
|
+
B --> D[compileMaxLength]
|
|
469
|
+
B --> E[compilePattern]
|
|
470
|
+
|
|
471
|
+
C --> F{min > 0?}
|
|
472
|
+
D --> G{max >= 0?}
|
|
473
|
+
E --> H{pattern valid?}
|
|
474
|
+
|
|
475
|
+
F -->|No| I[undefined]
|
|
476
|
+
F -->|Yes| J[minLength validator]
|
|
477
|
+
G -->|No| K[undefined]
|
|
478
|
+
G -->|Yes| L[maxLength validator]
|
|
479
|
+
H -->|No| M[undefined]
|
|
480
|
+
H -->|Yes| N[pattern validator]
|
|
481
|
+
|
|
482
|
+
I & J & K & L & M & N --> O{Any validators?}
|
|
483
|
+
|
|
484
|
+
O -->|No| P[Return undefined]
|
|
485
|
+
O -->|Yes| Q{Fast path?}
|
|
486
|
+
|
|
487
|
+
Q -->|Max only| R[maxOnly validator]
|
|
488
|
+
Q -->|Min only| S[minOnly validator]
|
|
489
|
+
Q -->|Pattern only| T[patternOnly validator]
|
|
490
|
+
Q -->|Complex| U[full validator]
|
|
491
|
+
|
|
492
|
+
style P fill:#f99
|
|
493
|
+
style R fill:#9f9
|
|
494
|
+
style S fill:#9f9
|
|
495
|
+
style T fill:#9f9
|
|
496
|
+
style U fill:#9f9
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
---
|
|
500
|
+
|
|
501
|
+
## Reference Resolution System
|
|
502
|
+
|
|
503
|
+
The `$ref` resolution is one of the most critical performance optimizations. Rather than resolving references at validation time, all references are pre-compiled:
|
|
504
|
+
|
|
505
|
+
```mermaid
|
|
506
|
+
flowchart TD
|
|
507
|
+
A[Schema with $ref] --> B[Traverse Schema]
|
|
508
|
+
B --> C[Build Schema Map]
|
|
509
|
+
C --> D[Store all $id locations]
|
|
510
|
+
C --> E[Store null placeholders for $refs]
|
|
511
|
+
|
|
512
|
+
E --> F[restoreSchemaRefsInMap]
|
|
513
|
+
F --> G[resolveRefSchemaDeep]
|
|
514
|
+
|
|
515
|
+
G --> H{Chain resolution}
|
|
516
|
+
H --> I[Flatten a→b→c to a→c]
|
|
517
|
+
|
|
518
|
+
I --> J[Pre-compile ValidationObjects]
|
|
519
|
+
J --> K[Cache by URI]
|
|
520
|
+
|
|
521
|
+
K --> L[Validation time]
|
|
522
|
+
L --> M{Ref in cache?}
|
|
523
|
+
M -->|Yes| N[Direct lookup O1]
|
|
524
|
+
M -->|No| O[Lazy resolve fallback]
|
|
525
|
+
|
|
526
|
+
style N fill:#9f9
|
|
527
|
+
style O fill:#ff9
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
### JSON Pointer Handling
|
|
531
|
+
|
|
532
|
+
```mermaid
|
|
533
|
+
classDiagram
|
|
534
|
+
class TraverseOptions {
|
|
535
|
+
+string origin
|
|
536
|
+
+boolean mergeSchemas
|
|
537
|
+
+boolean anchorsGlobal
|
|
538
|
+
+boolean anchorsAllowed
|
|
539
|
+
+boolean skipErrors
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
class JsonPointer {
|
|
543
|
+
+string id
|
|
544
|
+
+string search
|
|
545
|
+
+string leftUri
|
|
546
|
+
+string fragment
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
TraverseOptions --|> JsonPointerOptions
|
|
550
|
+
JsonPointerOptions --> JsonPointer : creates via createJsonPointer()
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
---
|
|
554
|
+
|
|
555
|
+
## Data Reference Systems
|
|
556
|
+
|
|
557
|
+
The validator implements two complementary data reference mechanisms:
|
|
558
|
+
|
|
559
|
+
```mermaid
|
|
560
|
+
flowchart TB
|
|
561
|
+
subgraph "$data Keyword Ajv-style"
|
|
562
|
+
A1[Relative JSON Pointer] --> A2[resolveRelativePointer]
|
|
563
|
+
A2 --> A3[Access parent/ancestor data]
|
|
564
|
+
A3 --> A4[Dynamic constraint values]
|
|
565
|
+
A4 --> A5[Example: maximum: {$data: '1/larger'}]
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
subgraph "data Keyword json-everything-style"
|
|
569
|
+
B1[JSON Pointer Path] --> B2[resolveDataRef]
|
|
570
|
+
B2 --> B3[Access from data root]
|
|
571
|
+
B3 --> B4[Cross-property validation]
|
|
572
|
+
B4 --> B5[Example: data: {minimum: '/A'}]
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
A5 --> C[Runtime value resolution]
|
|
576
|
+
B5 --> C
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
---
|
|
580
|
+
|
|
581
|
+
## Error Handling Architecture
|
|
582
|
+
|
|
583
|
+
```mermaid
|
|
584
|
+
flowchart LR
|
|
585
|
+
A[Validation Failure] --> B{skipErrors?}
|
|
586
|
+
|
|
587
|
+
B -->|true| C[Return false only]
|
|
588
|
+
B -->|false| D[Create InternalValidationError]
|
|
589
|
+
|
|
590
|
+
D --> E[Store in ValidationRoot.errors]
|
|
591
|
+
E --> F[Continue validation]
|
|
592
|
+
|
|
593
|
+
F --> G[All validators complete]
|
|
594
|
+
G --> H{collectErrors?}
|
|
595
|
+
|
|
596
|
+
H -->|true| I[Convert to ValidationError]
|
|
597
|
+
H -->|false| J[Return boolean only]
|
|
598
|
+
|
|
599
|
+
I --> K[Add human-readable messages]
|
|
600
|
+
K --> L[Return {valid, errors}]
|
|
601
|
+
|
|
602
|
+
style C fill:#9f9
|
|
603
|
+
style J fill:#9f9
|
|
604
|
+
style L fill:#9f9
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
### Error Handler Creation
|
|
608
|
+
|
|
609
|
+
```mermaid
|
|
610
|
+
sequenceDiagram
|
|
611
|
+
participant Compiler
|
|
612
|
+
participant VO as ValidationObject
|
|
613
|
+
participant VR as ValidationRoot
|
|
614
|
+
participant User
|
|
615
|
+
|
|
616
|
+
Compiler->>VO: createErrorHandler(expected, keyword)
|
|
617
|
+
VO->>VR: check options.skipErrors
|
|
618
|
+
|
|
619
|
+
alt skipErrors = true
|
|
620
|
+
VR-->>VO: fast no-op handler
|
|
621
|
+
VO-->>Compiler: () => false
|
|
622
|
+
else
|
|
623
|
+
VR-->>VO: full error handler
|
|
624
|
+
VO-->>Compiler: (data, ...meta) => {...}
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
Compiler->>User: validation function with handlers
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
---
|
|
631
|
+
|
|
632
|
+
## Performance Optimizations
|
|
633
|
+
|
|
634
|
+
### 1. Fast Path Specialization
|
|
635
|
+
|
|
636
|
+
```mermaid
|
|
637
|
+
flowchart TD
|
|
638
|
+
A[Schema Analysis] --> B{Common Pattern?}
|
|
639
|
+
|
|
640
|
+
B -->|type only| C[Inline type check]
|
|
641
|
+
B -->|required only| D[Inline required check]
|
|
642
|
+
B -->|minLength only| E[Direct length compare]
|
|
643
|
+
B -->|Complex| F[Full composition]
|
|
644
|
+
|
|
645
|
+
C --> G[No function call overhead]
|
|
646
|
+
D --> G
|
|
647
|
+
E --> G
|
|
648
|
+
F --> H[Minimal overhead]
|
|
649
|
+
|
|
650
|
+
G --> I[Maximum Performance]
|
|
651
|
+
H --> I
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
### 2. Validator Composition Strategies
|
|
655
|
+
|
|
656
|
+
Based on the number of validators, different composition strategies are used:
|
|
657
|
+
|
|
658
|
+
| Validator Count | Strategy | Implementation |
|
|
659
|
+
|-----------------|----------|----------------|
|
|
660
|
+
| 0 | No-op | `trueThat` |
|
|
661
|
+
| 1 | Direct | Return validator directly |
|
|
662
|
+
| 2 | Unrolled | `return v1(data) && v2(data)` |
|
|
663
|
+
| 3 | Unrolled | `return v1(data) && v2(data) && v3(data)` |
|
|
664
|
+
| 4+ | Loop | `for (let i = 0; i < n; i++)` |
|
|
665
|
+
|
|
666
|
+
### 3. String Length Optimization
|
|
667
|
+
|
|
668
|
+
```mermaid
|
|
669
|
+
flowchart TD
|
|
670
|
+
A[String validation] --> B{useGrapheme?}
|
|
671
|
+
|
|
672
|
+
B -->|false| C[data.length]
|
|
673
|
+
B -->|true| D{ASCII check}
|
|
674
|
+
|
|
675
|
+
D -->|All ASCII| E[data.length]
|
|
676
|
+
D -->|Non-ASCII| F{Cluster-forming chars?}
|
|
677
|
+
|
|
678
|
+
F -->|No| J[Code point count]
|
|
679
|
+
F -->|Yes| K[Segmenter iteration]
|
|
680
|
+
|
|
681
|
+
C --> G[Fast path]
|
|
682
|
+
E --> G
|
|
683
|
+
J --> G
|
|
684
|
+
K --> H[Slow path]
|
|
685
|
+
|
|
686
|
+
G --> I[Inline in hot loop]
|
|
687
|
+
H --> I
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
---
|
|
691
|
+
|
|
692
|
+
## Module Dependencies
|
|
693
|
+
|
|
694
|
+
```mermaid
|
|
695
|
+
flowchart TB
|
|
696
|
+
subgraph "@jarenjs/validate"
|
|
697
|
+
direction TB
|
|
698
|
+
INDEX[index.js]
|
|
699
|
+
SCHEMA[schema.js]
|
|
700
|
+
|
|
701
|
+
subgraph "Type Compilers"
|
|
702
|
+
STR[string.js]
|
|
703
|
+
NUM[number.js]
|
|
704
|
+
BINT[bigint.js]
|
|
705
|
+
ARR[array.js]
|
|
706
|
+
OBJ[object.js]
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
subgraph "Logic Compilers"
|
|
710
|
+
COMB[combine.js]
|
|
711
|
+
COND[condition.js]
|
|
712
|
+
ENUM[enum.js]
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
subgraph "Content Compilers"
|
|
716
|
+
FMT[format.js]
|
|
717
|
+
CONT[content.js]
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
subgraph "Data Reference"
|
|
721
|
+
DATA[data.js]
|
|
722
|
+
DDLR[dollar-data.js]
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
subgraph "Infrastructure"
|
|
726
|
+
TRAV[traverse.js]
|
|
727
|
+
TOOLS[tools.js]
|
|
728
|
+
end
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
subgraph "@jarenjs/core"
|
|
732
|
+
CORE[index.js]
|
|
733
|
+
CORE_NUM[number.js]
|
|
734
|
+
CORE_STR[string.js]
|
|
735
|
+
CORE_ARR[array.js]
|
|
736
|
+
CORE_OBJ[object.js]
|
|
737
|
+
CORE_FN[function.js]
|
|
738
|
+
CORE_TXT[text/index.js]
|
|
739
|
+
end
|
|
740
|
+
|
|
741
|
+
subgraph "@jarenjs/json"
|
|
742
|
+
CORE_JSON[index.js]
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
INDEX --> SCHEMA & TRAV & TOOLS & FMT
|
|
746
|
+
SCHEMA --> STR & NUM & BINT & ARR & OBJ & COMB & COND & ENUM & CONT & DATA & DDLR
|
|
747
|
+
|
|
748
|
+
INDEX --> CORE
|
|
749
|
+
SCHEMA --> CORE
|
|
750
|
+
STR --> CORE_STR & CORE_FN
|
|
751
|
+
NUM --> CORE_NUM & CORE_FN
|
|
752
|
+
BINT --> CORE_NUM & CORE_FN
|
|
753
|
+
ARR --> CORE_ARR & CORE_OBJ & CORE_FN
|
|
754
|
+
OBJ --> CORE_OBJ & CORE_ARR & CORE_FN
|
|
755
|
+
ENUM --> CORE_OBJ
|
|
756
|
+
CONT --> CORE_TXT & CORE_JSON
|
|
757
|
+
DATA --> CORE_JSON
|
|
758
|
+
DDLR --> CORE_JSON
|
|
759
|
+
TOOLS --> CORE & CORE_STR & CORE_ARR
|
|
760
|
+
TRAV --> CORE & CORE_NUM & CORE_STR & CORE_TXT
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
---
|
|
764
|
+
|
|
765
|
+
## Extension Points
|
|
766
|
+
|
|
767
|
+
### Format Registration
|
|
768
|
+
|
|
769
|
+
```mermaid
|
|
770
|
+
flowchart LR
|
|
771
|
+
A[Custom Format] --> B[Format Compiler]
|
|
772
|
+
B --> C[Returns Validator Function]
|
|
773
|
+
C --> D[Register via addFormat]
|
|
774
|
+
D --> E[Available in schemas]
|
|
775
|
+
|
|
776
|
+
style A fill:#9cf
|
|
777
|
+
style E fill:#9f9
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
### Custom Keywords
|
|
781
|
+
|
|
782
|
+
To add custom keywords, you would extend the compilation pipeline in `schema.js`:
|
|
783
|
+
|
|
784
|
+
```javascript
|
|
785
|
+
// In compileSchemaObject or a new module
|
|
786
|
+
function compileCustomKeyword(schemaObj, jsonSchema) {
|
|
787
|
+
if (jsonSchema.myKeyword === undefined) return undefined;
|
|
788
|
+
|
|
789
|
+
const addError = schemaObj.createErrorHandler(value, 'myKeyword');
|
|
790
|
+
|
|
791
|
+
return function validateMyKeyword(data, dataPath) {
|
|
792
|
+
// Custom validation logic
|
|
793
|
+
return isValid(data) || addError(data, dataPath);
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
---
|
|
799
|
+
|
|
800
|
+
## Testing Strategy
|
|
801
|
+
|
|
802
|
+
The architecture supports comprehensive testing at multiple levels:
|
|
803
|
+
|
|
804
|
+
```mermaid
|
|
805
|
+
flowchart TB
|
|
806
|
+
subgraph "Unit Tests"
|
|
807
|
+
A[Individual Compilers]
|
|
808
|
+
B[Utility Functions]
|
|
809
|
+
C[Type Checkers]
|
|
810
|
+
end
|
|
811
|
+
|
|
812
|
+
subgraph "Integration Tests"
|
|
813
|
+
D[Full Schema Compilation]
|
|
814
|
+
E[Reference Resolution]
|
|
815
|
+
F[Error Collection]
|
|
816
|
+
end
|
|
817
|
+
|
|
818
|
+
subgraph "Compliance Tests"
|
|
819
|
+
G[JSON Schema Test Suite]
|
|
820
|
+
H[Draft 6/7/2019-09/2020-12]
|
|
821
|
+
end
|
|
822
|
+
|
|
823
|
+
A & B & C --> I[Test Coverage]
|
|
824
|
+
D & E & F --> I
|
|
825
|
+
G & H --> I
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
---
|
|
829
|
+
|
|
830
|
+
## Data Structures
|
|
831
|
+
|
|
832
|
+
### schemasMap Structure
|
|
833
|
+
|
|
834
|
+
```typescript
|
|
835
|
+
Map<string, schema | null> {
|
|
836
|
+
// Root schemas
|
|
837
|
+
"http://example.com/schema#" → { type: "object", properties: {...} }
|
|
838
|
+
|
|
839
|
+
// Internal refs (after Phase 2, these point to final schemas)
|
|
840
|
+
"#/$defs/foo" → { type: "string" } // Flattened: was A→B→C, now A→C
|
|
841
|
+
"#/$defs/bar" → { $ref: "#/$defs/foo" } // If not yet resolved
|
|
842
|
+
|
|
843
|
+
// Anchors
|
|
844
|
+
"http://example.com/schema#myAnchor" → { type: "number" }
|
|
845
|
+
"#myAnchor" → { type: "number" } // Global anchor
|
|
846
|
+
}
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
### ValidationRoot.#objects Structure
|
|
850
|
+
|
|
851
|
+
```typescript
|
|
852
|
+
Map<string, ValidationObject> {
|
|
853
|
+
"http://example.com/schema#" → ValidationObject {
|
|
854
|
+
#path: "http://example.com/schema#",
|
|
855
|
+
#validator: function validateObject(data, root) {...},
|
|
856
|
+
#schema: { type: "object", ... },
|
|
857
|
+
#root: ValidationRoot
|
|
858
|
+
},
|
|
859
|
+
|
|
860
|
+
"#/$defs/foo" → ValidationObject {
|
|
861
|
+
#path: "#/$defs/foo",
|
|
862
|
+
#validator: function validateString(data, root) {...}, // Direct reference
|
|
863
|
+
#schema: { type: "string" },
|
|
864
|
+
#root: ValidationRoot
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
---
|
|
870
|
+
|
|
871
|
+
## Key Functions Reference
|
|
872
|
+
|
|
873
|
+
| Function | File | Purpose |
|
|
874
|
+
|----------|------|---------|
|
|
875
|
+
| `storeSchemaIdsInMap` | traverse.js | BFS traversal storing all schema IDs and refs |
|
|
876
|
+
| `resolveRefSchemaShallow` | traverse.js | Resolve single ref hop (baseUri + fragment) |
|
|
877
|
+
| `resolveRefSchemaDeep` | traverse.js | Follow ref chain to final schema |
|
|
878
|
+
| `restoreSchemaRefsInMap` | traverse.js | Flatten all ref chains at load time |
|
|
879
|
+
| `createJsonPointer` | traverse.js | Parse URI into components (id, leftUri, fragment) |
|
|
880
|
+
| `JarenValidator.compile` | index.js | Main entry point for schema compilation, draft/vocabulary selection |
|
|
881
|
+
| `JarenValidator.#precompileRefs` | index.js | Pre-create ValidationObjects for all refs |
|
|
882
|
+
| `ValidationObject.compileValidator` | index.js | Compile validator, ref combination, dynamic-anchor registration |
|
|
883
|
+
| `ValidationRoot.resolveObject` | index.js | Resolve ref to ValidationObject (fallback only) |
|
|
884
|
+
| `wrapUnevaluated` | unevaluated.js | Final-stage unevaluatedProperties/unevaluatedItems check |
|
|
885
|
+
| `EvalLog` | tools.js | Annotation log with mark/rollback for unevaluated* |
|
|
886
|
+
| `collectDynamicAnchorsDeep` | dynamic-ref.js | Gather a resource's $dynamicAnchors for scope entry |
|
|
887
|
+
|
|
888
|
+
---
|
|
889
|
+
|
|
890
|
+
## Implementation Notes
|
|
891
|
+
|
|
892
|
+
### How $ref Works
|
|
893
|
+
|
|
894
|
+
1. **Draft 7 and earlier - $ref ignores siblings**: when a schema has `$ref`, all other keywords are ignored, the referenced schema completely replaces the current schema, and a sibling `$id` does not affect `$ref` resolution
|
|
895
|
+
2. **Draft 2019-09 and later - $ref has siblings**: `$ref` is just another keyword; sibling keywords apply together with the referenced schema, and a sibling `$id` DOES establish the base URI the `$ref` resolves against
|
|
896
|
+
3. **Location-independent identifiers**: Anchors like `#foo` should be resolvable within their document
|
|
897
|
+
4. **Ref chain resolution happens at compile time**: pre-resolved via `restoreSchemaRefsInMap` and `#precompileRefs`
|
|
898
|
+
|
|
899
|
+
### Base URI Resolution
|
|
900
|
+
|
|
901
|
+
When schemas have `$id` that changes the base URI:
|
|
902
|
+
|
|
903
|
+
```json
|
|
904
|
+
{
|
|
905
|
+
"$id": "http://example.com/schema",
|
|
906
|
+
"$defs": {
|
|
907
|
+
"nested": {
|
|
908
|
+
"$id": "nested/",
|
|
909
|
+
"$defs": {
|
|
910
|
+
"deep": {
|
|
911
|
+
"$ref": "folder/file.json" // Resolves against http://example.com/nested/
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
```
|
|
918
|
+
|
|
919
|
+
**Resolution Rules**:
|
|
920
|
+
1. `$id` changes the base URI for itself and all children
|
|
921
|
+
2. `$ref` is resolved against the current base URI
|
|
922
|
+
3. Relative `$id` values resolve against the parent's base URI
|
|
923
|
+
4. Trailing `#` is stripped for URL resolution
|
|
924
|
+
|
|
925
|
+
### ECMAScript Regex in JSON Schema
|
|
926
|
+
|
|
927
|
+
1. **Unicode flag required**: All regex patterns use the `u` flag for proper Unicode support
|
|
928
|
+
2. **Unicode property escapes**: Patterns using `\p{...}` require the `u` flag
|
|
929
|
+
3. **Surrogate pairs**: Non-BMP characters (like emojis) are represented as surrogate pairs in JavaScript
|
|
930
|
+
|
|
931
|
+
---
|
|
932
|
+
|
|
933
|
+
## Debugging Guide
|
|
934
|
+
|
|
935
|
+
### "Can not resolve schema for 'X'"
|
|
936
|
+
|
|
937
|
+
**Cause**: The ref `X` is not in schemasMap
|
|
938
|
+
|
|
939
|
+
**Check**:
|
|
940
|
+
- Was the schema containing `X` added via `addSchema()`?
|
|
941
|
+
- Is the ref path correct in the schema?
|
|
942
|
+
- After `restoreSchemaRefsInMap`, all refs should have non-null values
|
|
943
|
+
|
|
944
|
+
### Performance Degradation on First Validation
|
|
945
|
+
|
|
946
|
+
**Cause**: Refs being resolved at validation time instead of compile time
|
|
947
|
+
|
|
948
|
+
**Check**:
|
|
949
|
+
- Is `#precompileRefs` being called in `compile()`?
|
|
950
|
+
- Are ValidationObjects being created for all refs?
|
|
951
|
+
- Verify: `compileValidator` should find targets immediately without fallback
|
|
952
|
+
|
|
953
|
+
### Test Interference (tests pass individually but fail together)
|
|
954
|
+
|
|
955
|
+
**Cause**: Global shared state between validators
|
|
956
|
+
|
|
957
|
+
**Check**:
|
|
958
|
+
- Are you using a global cache? (Don't)
|
|
959
|
+
- Solution: All state should be per-ValidationRoot or per-JarenValidator
|
|
960
|
+
|
|
961
|
+
### Debugging Ref Resolution
|
|
962
|
+
|
|
963
|
+
Enable debug logging in traverse.js:
|
|
964
|
+
|
|
965
|
+
```javascript
|
|
966
|
+
// In packages/validate/src/traverse.js
|
|
967
|
+
console.log('Resolving ref:', ref, 'against baseUri:', baseUri);
|
|
968
|
+
```
|
|
969
|
+
|
|
970
|
+
---
|
|
971
|
+
|
|
972
|
+
## Related Documentation
|
|
973
|
+
|
|
974
|
+
For broader context on how this package fits into the JarenJS ecosystem:
|
|
975
|
+
|
|
976
|
+
- **Project-wide Architecture**: See `/ARCHITECTURE.md` at the project root
|
|
977
|
+
- **Developer Guide**: See `/HOWTO.md` at the project root
|
|
978
|
+
- **Core Package**: Depends on `@jarenjs/core` for fundamental utilities and `@jarenjs/json` for the JSON addressing standards
|
|
979
|
+
|
|
980
|
+
---
|
|
981
|
+
|
|
982
|
+
## Key Files Reference
|
|
983
|
+
|
|
984
|
+
| File | Purpose | Key Exports |
|
|
985
|
+
|------|---------|-------------|
|
|
986
|
+
| `index.js` | Public API | `JarenValidator`, `ValidationOptions`, `ValidatorOptions`, `ValidationError` |
|
|
987
|
+
| `schema.js` | Schema compilation | `compileSchemaObject` |
|
|
988
|
+
| `traverse.js` | Schema traversal | `TraverseOptions`, `storeSchemaIdsInMap`, `resolveRefSchemaDeep` |
|
|
989
|
+
| `tools.js` | Shared utilities | `isBoolOrObjectClass`, `hasSchemaRef`, `createIsSchemaTypeHandler` |
|
|
990
|
+
| `string.js` | String keywords | `compileStringBasic` |
|
|
991
|
+
| `number.js` | Number keywords | `compileNumberBasic` |
|
|
992
|
+
| `bigint.js` | BigInt keywords | `compileBigIntBasic` |
|
|
993
|
+
| `array.js` | Array keywords | `compileArraySchema` |
|
|
994
|
+
| `object.js` | Object keywords | `compileObjectSchema` |
|
|
995
|
+
| `combine.js` | Logic keywords | `compileCombineSchema` |
|
|
996
|
+
| `condition.js` | If/then/else | `compileConditionSchema` |
|
|
997
|
+
| `enum.js` | Enum/const | `compileEnumBasic` |
|
|
998
|
+
| `format.js` | Format registry | `registerFormatCompiler`, `compileFormatBasic` |
|
|
999
|
+
| `content.js` | Content encoding | `compileContentSchema` |
|
|
1000
|
+
| `data.js` | Data keyword | `compileDataSchema` |
|
|
1001
|
+
| `dollar-data.js` | $data keyword | `compileDollarDataSchema` |
|
|
1002
|
+
| `unevaluated.js` | unevaluated* keywords | `wrapUnevaluated` |
|
|
1003
|
+
| `dynamic-ref.js` | Dynamic scope helpers | `collectDynamicAnchorsDeep`, `hasRecursiveAnchor`, `getDynamicAnchorName` |
|
|
1004
|
+
|
|
1005
|
+
---
|
|
1006
|
+
|
|
1007
|
+
## Architectural Decisions
|
|
1008
|
+
|
|
1009
|
+
### 1. Compilation over Interpretation
|
|
1010
|
+
|
|
1011
|
+
**Decision**: Compile schemas to functions rather than interpret them at runtime.
|
|
1012
|
+
|
|
1013
|
+
**Rationale**:
|
|
1014
|
+
- Eliminates schema traversal during validation
|
|
1015
|
+
- Enables V8 optimization of hot paths
|
|
1016
|
+
- Allows fast-path specialization
|
|
1017
|
+
|
|
1018
|
+
**Trade-off**: Higher memory usage for storing compiled functions.
|
|
1019
|
+
|
|
1020
|
+
### 2. Pre-compiled References
|
|
1021
|
+
|
|
1022
|
+
**Decision**: Resolve all `$ref` at compile time, not validation time.
|
|
1023
|
+
|
|
1024
|
+
**Rationale**:
|
|
1025
|
+
- Eliminates recursive lookup overhead during validation
|
|
1026
|
+
- Flattens reference chains (a→b→c becomes a→c)
|
|
1027
|
+
- Enables circular reference detection at compile time
|
|
1028
|
+
|
|
1029
|
+
### 3. Lazy Error Generation
|
|
1030
|
+
|
|
1031
|
+
**Decision**: Only create error objects when `skipErrors` is false.
|
|
1032
|
+
|
|
1033
|
+
**Rationale**:
|
|
1034
|
+
- Most production use cases only need boolean results
|
|
1035
|
+
- Error object creation is expensive
|
|
1036
|
+
- Reduces GC pressure during high-throughput validation
|
|
1037
|
+
|
|
1038
|
+
### 4. Dual Data Reference Systems
|
|
1039
|
+
|
|
1040
|
+
**Decision**: Support both `$data` (Ajv-style) and `data` (json-everything-style) keywords.
|
|
1041
|
+
|
|
1042
|
+
**Rationale**:
|
|
1043
|
+
- Maximizes compatibility with existing schema ecosystems
|
|
1044
|
+
- Different use cases favor different reference styles
|
|
1045
|
+
- Minimal overhead when not used
|
|
1046
|
+
|
|
1047
|
+
### 5. Annotation Tracking via a Shared Log
|
|
1048
|
+
|
|
1049
|
+
**Decision**: Implement `unevaluatedProperties`/`unevaluatedItems` with a per-root `EvalLog` of `(instance, key)` pairs and mark/rollback semantics, compiled in only when the schema set uses these keywords.
|
|
1050
|
+
|
|
1051
|
+
**Rationale**:
|
|
1052
|
+
- Instance-identity keying makes annotations flow correctly through `$ref` chains and recursion without threading context through every validator signature
|
|
1053
|
+
- mark/rollback gives failed applicator branches (anyOf/oneOf/not/if) exact annotation-discarding semantics
|
|
1054
|
+
- The compile-time feature scan keeps schemas without unevaluated* keywords completely free of tracking overhead
|
|
1055
|
+
|
|
1056
|
+
### 6. Dynamic Scope as Per-Anchor Stacks
|
|
1057
|
+
|
|
1058
|
+
**Decision**: Track the dynamic scope for `$recursiveRef`/`$dynamicRef` as per-anchor-name validator stacks, pushed on resource entry (root validation and `$ref` crossings) and popped on exit.
|
|
1059
|
+
|
|
1060
|
+
**Rationale**:
|
|
1061
|
+
- Entering a resource registers ALL of its `$dynamicAnchor`s (collected once at compile time), matching the specification's resource-based dynamic scope
|
|
1062
|
+
- Outermost-first resolution is a bottom-of-stack read
|
|
1063
|
+
- Balanced push/pop via try/finally keeps the scope correct across validation failures
|
|
1064
|
+
|
|
1065
|
+
---
|
|
1066
|
+
|
|
1067
|
+
*This document was generated for contributors to understand the architecture of @jarenjs/validate. For implementation details, refer to the source code and inline JSDoc comments.*
|