@hyperjump/json-schema 1.1.1 → 1.2.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/README.md +123 -43
- package/bundle/index.d.ts +18 -0
- package/bundle/index.js +269 -0
- package/draft-2019-09/recursiveRef.js +1 -0
- package/draft-2020-12/dynamicRef.js +1 -0
- package/lib/media-types.js +1 -1
- package/lib/schema.js +22 -21
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -11,6 +11,9 @@ A collection of modules for working with JSON Schemas.
|
|
|
11
11
|
* Schemas can reference other schemas using a different dialect
|
|
12
12
|
* Work directly with schemas on the filesystem or HTTP
|
|
13
13
|
* Create custom keywords, vocabularies, and dialects
|
|
14
|
+
* Bundle multiple schemas into one document
|
|
15
|
+
* Uses the process defined in the 2020-12 specification but works with any
|
|
16
|
+
dialect.
|
|
14
17
|
* Provides utilities for building non-validation JSON Schema tooling
|
|
15
18
|
|
|
16
19
|
## Install
|
|
@@ -41,7 +44,12 @@ The API for this library is divided into two categories: Stable and
|
|
|
41
44
|
Experimental. The Stable API strictly follows semantic versioning, but the
|
|
42
45
|
Experimental API may have backward-incompatible changes between minor versions.
|
|
43
46
|
|
|
44
|
-
|
|
47
|
+
All experimental features are segregated into exports that include the word
|
|
48
|
+
"experimental" so you never accidentally depend on something that could change
|
|
49
|
+
or be removed in future releases.
|
|
50
|
+
|
|
51
|
+
## Validation
|
|
52
|
+
### Usage
|
|
45
53
|
This library supports many versions of JSON Schema. Use the pattern
|
|
46
54
|
`@hyperjump/json-schema/*` to import the version you need.
|
|
47
55
|
|
|
@@ -130,7 +138,7 @@ const isString = await validate(`file://${__dirname}/string.schema.yaml`);
|
|
|
130
138
|
const output = isString("foo");
|
|
131
139
|
```
|
|
132
140
|
|
|
133
|
-
**
|
|
141
|
+
**OpenAPI**
|
|
134
142
|
|
|
135
143
|
The OpenAPI 3.0 and 3.1 meta-schemas are pre-loaded and the OpenAPI JSON Schema
|
|
136
144
|
dialects for each of those versions is supported. A document with a Content-Type
|
|
@@ -155,7 +163,7 @@ const output = await validate("https://spec.openapis.org/oas/3.1/schema-base", o
|
|
|
155
163
|
const output = await validate(`file://${__dirname}/example.openapi.json#/components/schemas/foo`, 42);
|
|
156
164
|
```
|
|
157
165
|
|
|
158
|
-
|
|
166
|
+
### API
|
|
159
167
|
These are available from any of the exports that refer to a version of JSON
|
|
160
168
|
Schema, such as `@hyperjump/json-schema/draft-2020-12`.
|
|
161
169
|
|
|
@@ -178,12 +186,12 @@ Schema, such as `@hyperjump/json-schema/draft-2020-12`.
|
|
|
178
186
|
|
|
179
187
|
This error is thrown if the schema being compiled is found to be invalid.
|
|
180
188
|
The `output` field contains an `OutputUnit` with information about the
|
|
181
|
-
error. You can use the `
|
|
182
|
-
format that is returned in `output`.
|
|
183
|
-
* **
|
|
189
|
+
error. You can use the `setMetaSchemaOutputFormat` configuration to set the
|
|
190
|
+
output format that is returned in `output`.
|
|
191
|
+
* **setMetaSchemaOutputFormat**: (outputFormat: OutputFormat) => void
|
|
184
192
|
|
|
185
193
|
Set the output format used for validating schemas.
|
|
186
|
-
* **
|
|
194
|
+
* **getMetaSchemaOutputFormat**: () => OutputFormat
|
|
187
195
|
|
|
188
196
|
Get the output format used for validating schemas.
|
|
189
197
|
* **setShouldMetaValidate**: (isEnabled: boolean) => void
|
|
@@ -221,28 +229,92 @@ The following types are used in the above definitions
|
|
|
221
229
|
Given a filesystem path, return whether or not the file should be
|
|
222
230
|
considered a member of this media type.
|
|
223
231
|
|
|
224
|
-
##
|
|
225
|
-
The JSON Schema specification includes several features that are experimental in
|
|
226
|
-
nature including the Vocabulary System, Output Formats, and Annotations. This
|
|
227
|
-
implementation aims to support only the latest version of experimental features
|
|
228
|
-
as they evolve. There will not be a major version bump if there needs to be
|
|
229
|
-
backward incompatible changes to the Experimental API.
|
|
230
|
-
|
|
232
|
+
## Bundling
|
|
231
233
|
### Usage
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
234
|
+
You can bundle schemas with external references into single deliverable using
|
|
235
|
+
the official JSON Schema bundling process introduced in the 2020-12
|
|
236
|
+
specification. Given a schema with external references, any external schemas
|
|
237
|
+
will be embedded in the schema resulting in a Compound Schema Document with all
|
|
238
|
+
the schemas necessary to evaluate the given schema in one document.
|
|
239
|
+
|
|
240
|
+
The bundling process allows schemas to be embedded without needing to modify any
|
|
241
|
+
references which means you get the same output details whether you validate the
|
|
242
|
+
bundle or the original unbundled schemas.
|
|
235
243
|
|
|
236
244
|
```javascript
|
|
237
|
-
import {
|
|
245
|
+
import { addSchema } from "@hyperjump/json-schema/draft-2020-12";
|
|
246
|
+
import { bundle } from "@hyperjump/json-schema/bundle";
|
|
247
|
+
|
|
248
|
+
addSchema({
|
|
249
|
+
"$id": "https://example.com/main"
|
|
250
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
251
|
+
|
|
252
|
+
"type": "object",
|
|
253
|
+
"properties": {
|
|
254
|
+
"foo": { "$ref": "/string" }
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
addSchema({
|
|
259
|
+
"$id": "https://example.com/string",
|
|
260
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
261
|
+
|
|
262
|
+
"type": "string"
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const bundledSchema = await bundle("https://example.com/main"); // {
|
|
266
|
+
// "$id": "https://example.com/main",
|
|
267
|
+
// "$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
268
|
+
//
|
|
269
|
+
// "type": "object",
|
|
270
|
+
// "properties": {
|
|
271
|
+
// "foo": { "$ref": "/string" }
|
|
272
|
+
// },
|
|
273
|
+
//
|
|
274
|
+
// "$defs": {
|
|
275
|
+
// "https://example.com/main": {
|
|
276
|
+
// "$id": "https://example.com/main",
|
|
277
|
+
// "type": "string"
|
|
278
|
+
// }
|
|
279
|
+
// }
|
|
280
|
+
// }
|
|
238
281
|
```
|
|
239
282
|
|
|
283
|
+
### API
|
|
284
|
+
These are available from the `@hyperjump/json-schema/bundle` export.
|
|
285
|
+
|
|
286
|
+
* **bundle**: (uri: string, options: Options) => Promise<SchemaObject>
|
|
287
|
+
|
|
288
|
+
Create a bundled schema starting with the given schema. External schemas
|
|
289
|
+
will be fetched from the filesystem, the network, or internally as needed.
|
|
290
|
+
|
|
291
|
+
Options:
|
|
292
|
+
* alwaysIncludeDialect: boolean (default: false) -- Include dialect even
|
|
293
|
+
when it isn't strictly needed
|
|
294
|
+
* bundleMode: "flat" | "full" (default: "flat") -- When bundling schemas
|
|
295
|
+
that already contain bundled schemas, "flat" mode with remove nested
|
|
296
|
+
embedded schemas and put them all in the top level `$defs`. When using
|
|
297
|
+
"full" mode, it will keep the already embedded schemas around, which will
|
|
298
|
+
result in some embedded schema duplication.
|
|
299
|
+
* definitionNamingStrategy: "uri" | "uuid" (default: "uri") -- By default
|
|
300
|
+
the name used in definitions for embedded schemas will match the
|
|
301
|
+
identifier of the embedded schema. This naming is unlikely to collide
|
|
302
|
+
with actual definitions, but if you want to be sure, you can use the
|
|
303
|
+
"uuid" strategy instead to be sure you get a unique name.
|
|
304
|
+
* externalSchemas: string[] (default: []) -- A list of schemas URIs that
|
|
305
|
+
are available externally and should not be included in the bundle.
|
|
306
|
+
|
|
307
|
+
## Output Formats (Experimental)
|
|
308
|
+
### Usage
|
|
309
|
+
|
|
240
310
|
**Change the validation output format**
|
|
241
311
|
|
|
242
312
|
The `FLAG` output format isn't very informative. You can change the output
|
|
243
|
-
format used for validation to get more information.
|
|
313
|
+
format used for validation to get more information about failures.
|
|
244
314
|
|
|
245
315
|
```javascript
|
|
316
|
+
import { BASIC } from "@hyperjump/json-schema/experimental";
|
|
317
|
+
|
|
246
318
|
const output = await validate("https://example.com/schema1", 42, BASIC);
|
|
247
319
|
```
|
|
248
320
|
|
|
@@ -251,7 +323,10 @@ const output = await validate("https://example.com/schema1", 42, BASIC);
|
|
|
251
323
|
The output format used for validating schemas can be changed as well.
|
|
252
324
|
|
|
253
325
|
```javascript
|
|
254
|
-
|
|
326
|
+
import { validate, setMetaSchemaOutputFormat } from "@hyperjump/json-schema/draft-2020-12";
|
|
327
|
+
import { BASIC } from "@hyperjump/json-schema/experimental";
|
|
328
|
+
|
|
329
|
+
setMetaSchemaOutputFormat(BASIC);
|
|
255
330
|
try {
|
|
256
331
|
const output = await validate("https://example.com/invalid-schema");
|
|
257
332
|
} catch (error) {
|
|
@@ -259,8 +334,20 @@ try {
|
|
|
259
334
|
}
|
|
260
335
|
```
|
|
261
336
|
|
|
262
|
-
|
|
337
|
+
### API
|
|
338
|
+
**Type Definitions**
|
|
339
|
+
|
|
340
|
+
* **OutputFormat**: **FLAG** | **BASIC** | **DETAILED** | **VERBOSE**
|
|
341
|
+
|
|
342
|
+
In addition to the `FLAG` output format in the Stable API, the Experimental
|
|
343
|
+
API includes support for the `BASIC`, `DETAILED`, and `VERBOSE` formats as
|
|
344
|
+
specified in the 2019-09 specification (with some minor customizations).
|
|
345
|
+
This implementation doesn't include annotations or human readable error
|
|
346
|
+
messages. The output can be processed to create human readable error
|
|
347
|
+
messages as needed.
|
|
263
348
|
|
|
349
|
+
## Meta-Schemas, Keywords, Vocabularies, and Dialects (Experimental)
|
|
350
|
+
### Usage
|
|
264
351
|
In order to create and use a custom keyword, you need to define your keyword's
|
|
265
352
|
behavior, create a vocabulary that includes that keyword, and then create a
|
|
266
353
|
dialect that includes your vocabulary.
|
|
@@ -384,14 +471,6 @@ const output = await validate("https://example.com/schema1", 42); // Expect Inva
|
|
|
384
471
|
### API
|
|
385
472
|
These are available from the `@hyperjump/json-schema/experimental` export.
|
|
386
473
|
|
|
387
|
-
* **compile**: (schema: SchemaDocument) => Promise<CompiledSchema>
|
|
388
|
-
|
|
389
|
-
Return a compiled schema. This is useful if you're creating tooling for
|
|
390
|
-
something other than validation.
|
|
391
|
-
* **interpret**: (schema: CompiledSchema, instance: Instance, outputFormat: OutputFormat = BASIC) => OutputUnit
|
|
392
|
-
|
|
393
|
-
A curried function for validating an instance against a compiled schema.
|
|
394
|
-
This can be useful for creating custom output formats.
|
|
395
474
|
* **addKeyword**: (keywordHandler: Keyword) => void
|
|
396
475
|
|
|
397
476
|
Define a keyword for use in a vocabulary.
|
|
@@ -419,18 +498,6 @@ These are available from the `@hyperjump/json-schema/experimental` export.
|
|
|
419
498
|
A Keyword object that represents a "validate" operation. You would use this
|
|
420
499
|
for compiling and evaluating sub-schemas when defining a custom keyword.
|
|
421
500
|
|
|
422
|
-
**Type Definitions**
|
|
423
|
-
|
|
424
|
-
The following types are used in the above definitions
|
|
425
|
-
|
|
426
|
-
* **OutputFormat**: **FLAG** | **BASIC** | **DETAILED** | **VERBOSE**
|
|
427
|
-
|
|
428
|
-
In addition to the `FLAG` output format in the Stable API, the Experimental
|
|
429
|
-
API includes support for the `BASIC`, `DETAILED`, and `VERBOSE` formats as
|
|
430
|
-
specified in the 2019-09 specification (with some minor customizations).
|
|
431
|
-
This implementation doesn't include annotations or human readable error
|
|
432
|
-
messages. The output can be processed to create human readable error
|
|
433
|
-
messages as needed.
|
|
434
501
|
* **Keyword**: object
|
|
435
502
|
* id: string
|
|
436
503
|
|
|
@@ -459,7 +526,7 @@ The following types are used in the above definitions
|
|
|
459
526
|
If the keyword is an applicator, it will need to implements this
|
|
460
527
|
function for `unevaluatedItems` to work as expected.
|
|
461
528
|
|
|
462
|
-
### Schema
|
|
529
|
+
### Schema API
|
|
463
530
|
These functions are available from the
|
|
464
531
|
`@hyperjump/json-schema/schema/experimental` export.
|
|
465
532
|
|
|
@@ -518,7 +585,7 @@ The following types are used in the above definitions
|
|
|
518
585
|
* includeEmbedded: boolean (default: true) -- If false, embedded schemas
|
|
519
586
|
will be unbundled from the schema.
|
|
520
587
|
|
|
521
|
-
### Instance
|
|
588
|
+
### Instance API
|
|
522
589
|
These functions are available from the
|
|
523
590
|
`@hyperjump/json-schema/instance/experimental` export.
|
|
524
591
|
|
|
@@ -569,6 +636,19 @@ set of functions for working with InstanceDocuments.
|
|
|
569
636
|
|
|
570
637
|
Similar to `Array.prototype.length`.
|
|
571
638
|
|
|
639
|
+
## Low-level Utilities (Experimental)
|
|
640
|
+
### API
|
|
641
|
+
These are available from the `@hyperjump/json-schema/experimental` export.
|
|
642
|
+
|
|
643
|
+
* **compile**: (schema: SchemaDocument) => Promise<CompiledSchema>
|
|
644
|
+
|
|
645
|
+
Return a compiled schema. This is useful if you're creating tooling for
|
|
646
|
+
something other than validation.
|
|
647
|
+
* **interpret**: (schema: CompiledSchema, instance: Instance, outputFormat: OutputFormat = BASIC) => OutputUnit
|
|
648
|
+
|
|
649
|
+
A curried function for validating an instance against a compiled schema.
|
|
650
|
+
This can be useful for creating custom output formats.
|
|
651
|
+
|
|
572
652
|
## Contributing
|
|
573
653
|
|
|
574
654
|
### Tests
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { SchemaObject } from "../lib/schema.js";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
export const bundle: <A = SchemaObject>(uri: string, options?: BundleOptions) => Promise<A>;
|
|
5
|
+
export const FULL: "full";
|
|
6
|
+
export const FLAT: "flat";
|
|
7
|
+
export const URI: "uri";
|
|
8
|
+
export const UUID: "uuid";
|
|
9
|
+
|
|
10
|
+
export type BundleOptions = {
|
|
11
|
+
alwaysIncludeDialect?: boolean;
|
|
12
|
+
bundleMode?: BundleMode;
|
|
13
|
+
definitionNamingStrategy?: DefinitionNamingStrategy;
|
|
14
|
+
externalSchemas?: string[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type BundleMode = "full" | "flat";
|
|
18
|
+
export type DefinitionNamingStrategy = "uri" | "uuid";
|
package/bundle/index.js
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { v4 as uuid } from "uuid";
|
|
2
|
+
import * as JsonPointer from "@hyperjump/json-pointer";
|
|
3
|
+
import { toAbsoluteUri } from "../lib/common.js";
|
|
4
|
+
import { compile } from "../lib/core.js";
|
|
5
|
+
import { getKeywordName, getKeyword } from "../lib/keywords.js";
|
|
6
|
+
import Validation from "../lib/keywords/validation.js";
|
|
7
|
+
import * as Schema from "../lib/schema.js";
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
export const FULL = "full", FLAT = "flat";
|
|
11
|
+
export const URI = "uri", UUID = "uuid";
|
|
12
|
+
|
|
13
|
+
const defaultOptions = {
|
|
14
|
+
alwaysIncludeDialect: false,
|
|
15
|
+
bundleMode: FLAT,
|
|
16
|
+
definitionNamingStrategy: URI,
|
|
17
|
+
externalSchemas: []
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const bundle = async (url, options = {}) => {
|
|
21
|
+
loadKeywordSupport();
|
|
22
|
+
const fullOptions = { ...defaultOptions, ...options };
|
|
23
|
+
|
|
24
|
+
const schemaDoc = await Schema.get(url);
|
|
25
|
+
const externalIds = await collectExternalIds(url, fullOptions);
|
|
26
|
+
|
|
27
|
+
const bundled = Schema.toSchema(schemaDoc, {
|
|
28
|
+
includeEmbedded: fullOptions.bundleMode === FULL
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const bundlingLocation = "/" + getKeywordName(schemaDoc.dialectId, "https://json-schema.org/keyword/definitions");
|
|
32
|
+
if (JsonPointer.get(bundlingLocation, bundled) === undefined && externalIds.size > 0) {
|
|
33
|
+
JsonPointer.assign(bundlingLocation, bundled, {});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (const uri of externalIds.values()) {
|
|
37
|
+
const externalSchema = await Schema.get(uri);
|
|
38
|
+
const embeddedSchema = Schema.toSchema(externalSchema, {
|
|
39
|
+
parentId: schemaDoc.id,
|
|
40
|
+
parentDialect: fullOptions.alwaysIncludeDialect ? "" : schemaDoc.dialectId,
|
|
41
|
+
includeEmbedded: fullOptions.bundleMode === FULL
|
|
42
|
+
});
|
|
43
|
+
let id;
|
|
44
|
+
if (fullOptions.definitionNamingStrategy === URI) {
|
|
45
|
+
const idToken = getKeywordName(externalSchema.dialectId, "https://json-schema.org/keyword/id")
|
|
46
|
+
|| getKeywordName(externalSchema.dialectId, "https://json-schema.org/keyword/draft-04/id");
|
|
47
|
+
id = embeddedSchema[idToken];
|
|
48
|
+
} else if (fullOptions.definitionNamingStrategy === UUID) {
|
|
49
|
+
id = uuid();
|
|
50
|
+
} else {
|
|
51
|
+
throw Error(`Unknown definition naming stragety: ${fullOptions.definitionNamingStrategy}`);
|
|
52
|
+
}
|
|
53
|
+
const pointer = JsonPointer.append(id, bundlingLocation);
|
|
54
|
+
JsonPointer.assign(pointer, bundled, embeddedSchema);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return bundled;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const collectExternalIds = async (uri, options) => {
|
|
61
|
+
const { ast, schemaUri } = await compile(uri);
|
|
62
|
+
const subSchemaUris = new Set();
|
|
63
|
+
Validation.collectExternalIds(schemaUri, subSchemaUris, ast, {});
|
|
64
|
+
const externalIds = new Set([...subSchemaUris]
|
|
65
|
+
.map(toAbsoluteUri)
|
|
66
|
+
.filter((uri) => !options.externalSchemas.includes(uri)));
|
|
67
|
+
externalIds.delete(toAbsoluteUri(schemaUri));
|
|
68
|
+
|
|
69
|
+
return externalIds;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
Validation.collectExternalIds = (schemaUri, externalIds, ast, dynamicAnchors) => {
|
|
73
|
+
const nodes = ast[schemaUri][2];
|
|
74
|
+
if (externalIds.has(schemaUri) || typeof nodes === "boolean") {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
externalIds.add(schemaUri);
|
|
78
|
+
|
|
79
|
+
const id = toAbsoluteUri(schemaUri);
|
|
80
|
+
for (const [keywordId, , keywordValue] of nodes) {
|
|
81
|
+
const keyword = getKeyword(keywordId);
|
|
82
|
+
|
|
83
|
+
if (keyword.collectExternalIds) {
|
|
84
|
+
keyword.collectExternalIds(keywordValue, externalIds, ast, {
|
|
85
|
+
...ast.metaData[id].dynamicAnchors, ...dynamicAnchors
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const loadKeywordSupport = () => {
|
|
92
|
+
// Stable
|
|
93
|
+
|
|
94
|
+
const additionalProperties = getKeyword("https://json-schema.org/keyword/additionalProperties");
|
|
95
|
+
if (additionalProperties) {
|
|
96
|
+
additionalProperties.collectExternalIds = ([, , additionalProperties], externalIds, ast, dynamicAnchors) => {
|
|
97
|
+
if (typeof additionalProperties === "string") {
|
|
98
|
+
Validation.collectExternalIds(additionalProperties, externalIds, ast, dynamicAnchors);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const allOf = getKeyword("https://json-schema.org/keyword/allOf");
|
|
104
|
+
if (allOf) {
|
|
105
|
+
allOf.collectExternalIds = (allOf, externalIds, ast, dynamicAnchors) => {
|
|
106
|
+
allOf.forEach((schemaUri) => Validation.collectExternalIds(schemaUri, externalIds, ast, dynamicAnchors));
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const anyOf = getKeyword("https://json-schema.org/keyword/anyOf");
|
|
111
|
+
if (anyOf) {
|
|
112
|
+
anyOf.collectExternalIds = (anyOf, externalIds, ast, dynamicAnchors) => {
|
|
113
|
+
anyOf.forEach((schemaUri) => Validation.collectExternalIds(schemaUri, externalIds, ast, dynamicAnchors));
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const contains = getKeyword("https://json-schema.org/keyword/contains");
|
|
118
|
+
if (contains) {
|
|
119
|
+
contains.collectExternalIds = ({ contains }, externalIds, ast, dynamicAnchors) => {
|
|
120
|
+
Validation.collectExternalIds(contains, externalIds, ast, dynamicAnchors);
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const dependentSchemas = getKeyword("https://json-schema.org/keyword/dependentSchemas");
|
|
125
|
+
if (dependentSchemas) {
|
|
126
|
+
dependentSchemas.collectExternalIds = (dependentSchemas, externalIds, ast, dynamicAnchors) => {
|
|
127
|
+
Object.values(dependentSchemas).forEach(([, schemaUri]) => Validation.collectExternalIds(schemaUri, externalIds, ast, dynamicAnchors));
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const if_ = getKeyword("https://json-schema.org/keyword/if");
|
|
132
|
+
if (if_) {
|
|
133
|
+
if_.collectExternalIds = Validation.collectExternalIds;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const then = getKeyword("https://json-schema.org/keyword/then");
|
|
137
|
+
if (then) {
|
|
138
|
+
then.collectExternalIds = ([, then], externalIds, ast, dynamicAnchors) => {
|
|
139
|
+
Validation.collectExternalIds(then, externalIds, ast, dynamicAnchors);
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const else_ = getKeyword("https://json-schema.org/keyword/else");
|
|
144
|
+
if (else_) {
|
|
145
|
+
else_.collectExternalIds = ([, elseSchema], externalIds, ast, dynamicAnchors) => {
|
|
146
|
+
Validation.collectExternalIds(elseSchema, externalIds, ast, dynamicAnchors);
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const items = getKeyword("https://json-schema.org/keyword/items");
|
|
151
|
+
if (items) {
|
|
152
|
+
items.collectExternalIds = ([, items], externalIds, ast, dynamicAnchors) => {
|
|
153
|
+
Validation.collectExternalIds(items, externalIds, ast, dynamicAnchors);
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const not = getKeyword("https://json-schema.org/keyword/not");
|
|
158
|
+
if (not) {
|
|
159
|
+
not.collectExternalIds = Validation.collectExternalIds;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const oneOf = getKeyword("https://json-schema.org/keyword/oneOf");
|
|
163
|
+
if (oneOf) {
|
|
164
|
+
oneOf.collectExternalIds = (oneOf, externalIds, ast, dynamicAnchors) => {
|
|
165
|
+
oneOf.forEach((schemaUri) => Validation.collectExternalIds(schemaUri, externalIds, ast, dynamicAnchors));
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const patternProperties = getKeyword("https://json-schema.org/keyword/patternProperties");
|
|
170
|
+
if (patternProperties) {
|
|
171
|
+
patternProperties.collectExternalIds = (patternProperties, externalIds, ast, dynamicAnchors) => {
|
|
172
|
+
patternProperties.forEach(([, schemaUri]) => Validation.collectExternalIds(schemaUri, externalIds, ast, dynamicAnchors));
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const prefixItems = getKeyword("https://json-schema.org/keyword/prefixItems");
|
|
177
|
+
if (prefixItems) {
|
|
178
|
+
prefixItems.collectExternalIds = (tupleItems, externalIds, ast, dynamicAnchors) => {
|
|
179
|
+
tupleItems.forEach((schemaUri) => Validation.collectExternalIds(schemaUri, externalIds, ast, dynamicAnchors));
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const properties = getKeyword("https://json-schema.org/keyword/properties");
|
|
184
|
+
if (properties) {
|
|
185
|
+
properties.collectExternalIds = (properties, externalIds, ast, dynamicAnchors) => {
|
|
186
|
+
Object.values(properties).forEach((schemaUri) => Validation.collectExternalIds(schemaUri, externalIds, ast, dynamicAnchors));
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const propertyNames = getKeyword("https://json-schema.org/keyword/propertyNames");
|
|
191
|
+
if (propertyNames) {
|
|
192
|
+
propertyNames.collectExternalIds = Validation.collectExternalIds;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const ref = getKeyword("https://json-schema.org/keyword/ref");
|
|
196
|
+
if (ref) {
|
|
197
|
+
ref.collectExternalIds = Validation.collectExternalIds;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const unevaluatedItems = getKeyword("https://json-schema.org/keyword/unevaluatedItems");
|
|
201
|
+
if (unevaluatedItems) {
|
|
202
|
+
unevaluatedItems.collectExternalIds = ([, unevaluatedItems], externalIds, ast, dynamicAnchors) => {
|
|
203
|
+
Validation.collectExternalIds(unevaluatedItems, externalIds, ast, dynamicAnchors);
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const unevaluatedProperties = getKeyword("https://json-schema.org/keyword/unevaluatedProperties");
|
|
208
|
+
if (unevaluatedProperties) {
|
|
209
|
+
unevaluatedProperties.collectExternalIds = ([, unevaluatedProperties], externalIds, ast, dynamicAnchors) => {
|
|
210
|
+
Validation.collectExternalIds(unevaluatedProperties, externalIds, ast, dynamicAnchors);
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Draft-04
|
|
215
|
+
|
|
216
|
+
const additionalItems4 = getKeyword("https://json-schema.org/keyword/draft-04/additionalItems");
|
|
217
|
+
if (additionalItems4) {
|
|
218
|
+
additionalItems4.collectExternalIds = ([, additionalItems], externalIds, ast, dynamicAnchors) => {
|
|
219
|
+
if (typeof additionalItems === "string") {
|
|
220
|
+
Validation.collectExternalIds(additionalItems, externalIds, ast, dynamicAnchors);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const dependencies = getKeyword("https://json-schema.org/keyword/draft-04/dependencies");
|
|
226
|
+
if (dependencies) {
|
|
227
|
+
dependencies.collectExternalIds = (dependentSchemas, externalIds, ast, dynamicAnchors) => {
|
|
228
|
+
Object.values(dependentSchemas).forEach(([, dependency]) => {
|
|
229
|
+
if (typeof dependency === "string") {
|
|
230
|
+
Validation.collectExternalIds(dependency, externalIds, ast, dynamicAnchors);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const items4 = getKeyword("https://json-schema.org/keyword/draft-04/items");
|
|
237
|
+
if (items4) {
|
|
238
|
+
items4.collectExternalIds = (items, externalIds, ast, dynamicAnchors) => {
|
|
239
|
+
if (typeof items === "string") {
|
|
240
|
+
Validation.collectExternalIds(items, externalIds, ast, dynamicAnchors);
|
|
241
|
+
} else {
|
|
242
|
+
items.forEach((schemaUri) => Validation.collectExternalIds(schemaUri, externalIds, ast, dynamicAnchors));
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const ref4 = getKeyword("https://json-schema.org/keyword/draft-04/ref");
|
|
248
|
+
if (ref4) {
|
|
249
|
+
ref4.collectExternalIds = Validation.collectExternalIds;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Draft-06
|
|
253
|
+
|
|
254
|
+
const contains6 = getKeyword("https://json-schema.org/keyword/draft-06/contains");
|
|
255
|
+
if (contains6) {
|
|
256
|
+
contains6.collectExternalIds = (contains, externalIds, ast, dynamicAnchors) => {
|
|
257
|
+
Validation.collectExternalIds(contains, externalIds, ast, dynamicAnchors);
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Draft-2019-09
|
|
262
|
+
|
|
263
|
+
const contains19 = getKeyword("https://json-schema.org/keyword/draft-2019-09/contains");
|
|
264
|
+
if (contains19) {
|
|
265
|
+
contains19.collectExternalIds = ({ contains }, externalIds, ast, dynamicAnchors) => {
|
|
266
|
+
Validation.collectExternalIds(contains, externalIds, ast, dynamicAnchors);
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
};
|
|
@@ -7,6 +7,7 @@ const compile = (schema) => schema.id;
|
|
|
7
7
|
|
|
8
8
|
const interpret = (id, instance, ast, dynamicAnchors) => {
|
|
9
9
|
if ("" in ast.metaData[id].dynamicAnchors) {
|
|
10
|
+
dynamicAnchors = { ...ast.metaData[id].dynamicAnchors, ...dynamicAnchors };
|
|
10
11
|
return Validation.interpret(dynamicAnchors[""], instance, ast, dynamicAnchors);
|
|
11
12
|
} else {
|
|
12
13
|
return Validation.interpret(`${id}#`, instance, ast, dynamicAnchors);
|
|
@@ -14,6 +14,7 @@ const compile = async (dynamicRef, ast) => {
|
|
|
14
14
|
|
|
15
15
|
const interpret = ([id, fragment, ref], instance, ast, dynamicAnchors) => {
|
|
16
16
|
if (fragment in ast.metaData[id].dynamicAnchors) {
|
|
17
|
+
dynamicAnchors = { ...ast.metaData[id].dynamicAnchors, ...dynamicAnchors };
|
|
17
18
|
return Validation.interpret(dynamicAnchors[fragment], instance, ast, dynamicAnchors);
|
|
18
19
|
} else {
|
|
19
20
|
return Validation.interpret(ref, instance, ast, dynamicAnchors);
|
package/lib/media-types.js
CHANGED
|
@@ -7,7 +7,7 @@ export const addMediaTypePlugin = (contentType, plugin) => {
|
|
|
7
7
|
mediaTypePlugins[contentType] = plugin;
|
|
8
8
|
};
|
|
9
9
|
|
|
10
|
-
export const
|
|
10
|
+
export const parseResponse = (response) => {
|
|
11
11
|
const contentType = contentTypeParser.parse(response.headers.get("content-type"));
|
|
12
12
|
if (!(contentType.type in mediaTypePlugins)) {
|
|
13
13
|
throw Error(`${response.url} is not a schema. Found a document with media type: ${contentType.type}`);
|
package/lib/schema.js
CHANGED
|
@@ -4,8 +4,8 @@ import * as Json from "@hyperjump/json";
|
|
|
4
4
|
import * as JsonPointer from "@hyperjump/json-pointer";
|
|
5
5
|
import { jsonTypeOf, resolveUri, toAbsoluteUri, uriFragment, pathRelative } from "./common.js";
|
|
6
6
|
import fetch from "./fetch.js";
|
|
7
|
-
import
|
|
8
|
-
import
|
|
7
|
+
import { hasDialect, loadDialect, getKeywordName } from "./keywords.js";
|
|
8
|
+
import { parseResponse } from "./media-types.js";
|
|
9
9
|
import * as Reference from "./reference.js";
|
|
10
10
|
|
|
11
11
|
|
|
@@ -22,13 +22,13 @@ export const add = (schema, retrievalUri = undefined, contextDialectId = undefin
|
|
|
22
22
|
const dialectId = toAbsoluteUri(schema.$schema || contextDialectId || defaultDialectId);
|
|
23
23
|
delete schema.$schema;
|
|
24
24
|
|
|
25
|
-
if (!
|
|
25
|
+
if (!hasDialect(dialectId)) {
|
|
26
26
|
throw Error(`Encountered unknown dialect '${dialectId}'`);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
// Identifiers
|
|
30
|
-
const idToken =
|
|
31
|
-
||
|
|
30
|
+
const idToken = getKeywordName(dialectId, "https://json-schema.org/keyword/id")
|
|
31
|
+
|| getKeywordName(dialectId, "https://json-schema.org/keyword/draft-04/id");
|
|
32
32
|
if (retrievalUri === undefined && !(idToken in schema)) {
|
|
33
33
|
throw Error(`Unable to determine an identifier for the schema. Use the '${idToken}' keyword or pass a retrievalUri when loading the schema.`);
|
|
34
34
|
}
|
|
@@ -41,19 +41,19 @@ export const add = (schema, retrievalUri = undefined, contextDialectId = undefin
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
// Vocabulary
|
|
44
|
-
const vocabularyToken =
|
|
44
|
+
const vocabularyToken = getKeywordName(dialectId, "https://json-schema.org/keyword/vocabulary");
|
|
45
45
|
if (jsonTypeOf(schema[vocabularyToken], "object")) {
|
|
46
46
|
const allowUnknownKeywords = schema[vocabularyToken]["https://json-schema.org/draft/2019-09/vocab/core"]
|
|
47
47
|
|| schema[vocabularyToken]["https://json-schema.org/draft/2020-12/vocab/core"];
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
loadDialect(id, schema[vocabularyToken], allowUnknownKeywords);
|
|
50
50
|
delete schema[vocabularyToken];
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
const dynamicAnchors = {};
|
|
54
54
|
|
|
55
55
|
// Recursive anchor
|
|
56
|
-
const recursiveAnchorToken =
|
|
56
|
+
const recursiveAnchorToken = getKeywordName(dialectId, "https://json-schema.org/keyword/draft-2019-09/recursiveAnchor");
|
|
57
57
|
if (schema[recursiveAnchorToken] === true) {
|
|
58
58
|
dynamicAnchors[""] = `${id}#`;
|
|
59
59
|
}
|
|
@@ -76,7 +76,7 @@ export const add = (schema, retrievalUri = undefined, contextDialectId = undefin
|
|
|
76
76
|
const processSchema = (subject, id, dialectId, pointer, anchors, dynamicAnchors) => {
|
|
77
77
|
if (jsonTypeOf(subject, "object")) {
|
|
78
78
|
// Legacy id
|
|
79
|
-
const legacyIdToken =
|
|
79
|
+
const legacyIdToken = getKeywordName(dialectId, "https://json-schema.org/keyword/draft-04/id");
|
|
80
80
|
if (typeof subject[legacyIdToken] === "string") {
|
|
81
81
|
if (subject[legacyIdToken][0] === "#") {
|
|
82
82
|
const anchor = decodeURIComponent(subject[legacyIdToken].slice(1));
|
|
@@ -92,7 +92,7 @@ const processSchema = (subject, id, dialectId, pointer, anchors, dynamicAnchors)
|
|
|
92
92
|
|
|
93
93
|
// Embedded Schema
|
|
94
94
|
const embeddedDialectId = typeof subject.$schema === "string" ? toAbsoluteUri(subject.$schema) : dialectId;
|
|
95
|
-
const idToken =
|
|
95
|
+
const idToken = getKeywordName(embeddedDialectId, "https://json-schema.org/keyword/id");
|
|
96
96
|
if (typeof subject[idToken] === "string") {
|
|
97
97
|
subject[idToken] = resolveUri(subject[idToken], id);
|
|
98
98
|
add(subject, undefined, dialectId);
|
|
@@ -100,27 +100,27 @@ const processSchema = (subject, id, dialectId, pointer, anchors, dynamicAnchors)
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
// Legacy dynamic anchor
|
|
103
|
-
const legacyDynamicAnchorToken =
|
|
103
|
+
const legacyDynamicAnchorToken = getKeywordName(dialectId, "https://json-schema.org/keyword/draft-2020-12/dynamicAnchor");
|
|
104
104
|
if (typeof subject[legacyDynamicAnchorToken] === "string") {
|
|
105
105
|
dynamicAnchors[subject[legacyDynamicAnchorToken]] = `${id}#${encodeURI(pointer)}`;
|
|
106
106
|
anchors[subject[legacyDynamicAnchorToken]] = pointer;
|
|
107
107
|
delete subject[legacyDynamicAnchorToken];
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
const dynamicAnchorToken =
|
|
110
|
+
const dynamicAnchorToken = getKeywordName(dialectId, "https://json-schema.org/keyword/dynamicAnchor");
|
|
111
111
|
if (typeof subject[dynamicAnchorToken] === "string") {
|
|
112
112
|
dynamicAnchors[subject[dynamicAnchorToken]] = `${id}#${encodeURI(pointer)}`;
|
|
113
113
|
delete subject[dynamicAnchorToken];
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
const anchorToken =
|
|
116
|
+
const anchorToken = getKeywordName(dialectId, "https://json-schema.org/keyword/anchor");
|
|
117
117
|
if (typeof subject[anchorToken] === "string") {
|
|
118
118
|
anchors[subject[anchorToken]] = pointer;
|
|
119
119
|
delete subject[anchorToken];
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
// Legacy $ref
|
|
123
|
-
const jrefToken =
|
|
123
|
+
const jrefToken = getKeywordName(dialectId, "https://json-schema.org/keyword/draft-04/ref");
|
|
124
124
|
if (typeof subject[jrefToken] === "string") {
|
|
125
125
|
return Reference.cons(subject[jrefToken], subject);
|
|
126
126
|
}
|
|
@@ -168,11 +168,11 @@ export const get = async (url, contextDoc = nil) => {
|
|
|
168
168
|
throw Error(`Failed to retrieve schema with id: ${id}`);
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
-
const [schema, contextDialectId] = await
|
|
171
|
+
const [schema, contextDialectId] = await parseResponse(response);
|
|
172
172
|
|
|
173
173
|
// Try to determine the dialect from the meta-schema if it isn't already known
|
|
174
174
|
const dialectId = toAbsoluteUri(schema.$schema || contextDialectId || defaultDialectId);
|
|
175
|
-
if (!
|
|
175
|
+
if (!hasDialect(dialectId) && !hasStoredSchema(dialectId)) {
|
|
176
176
|
await get(dialectId);
|
|
177
177
|
}
|
|
178
178
|
|
|
@@ -241,11 +241,12 @@ const toSchemaDefaultOptions = {
|
|
|
241
241
|
export const toSchema = (schemaDoc, options = {}) => {
|
|
242
242
|
const fullOptions = { ...toSchemaDefaultOptions, ...options };
|
|
243
243
|
|
|
244
|
-
const idToken =
|
|
245
|
-
|
|
246
|
-
const
|
|
247
|
-
const
|
|
248
|
-
const
|
|
244
|
+
const idToken = getKeywordName(schemaDoc.dialectId, "https://json-schema.org/keyword/id")
|
|
245
|
+
|| getKeywordName(schemaDoc.dialectId, "https://json-schema.org/keyword/draft-04/id");
|
|
246
|
+
const anchorToken = getKeywordName(schemaDoc.dialectId, "https://json-schema.org/keyword/anchor");
|
|
247
|
+
const dynamicAnchorToken = getKeywordName(schemaDoc.dialectId, "https://json-schema.org/keyword/dynamicAnchor");
|
|
248
|
+
const legacyDynamicAnchorToken = getKeywordName(schemaDoc.dialectId, "https://json-schema.org/keyword/draft-2020-12/dynamicAnchor");
|
|
249
|
+
const recursiveAnchorToken = getKeywordName(schemaDoc.dialectId, "https://json-schema.org/keyword/recursiveAnchor");
|
|
249
250
|
|
|
250
251
|
const anchors = {};
|
|
251
252
|
for (const anchor in schemaDoc.anchors) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperjump/json-schema",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "A JSON Schema validator with support for custom keywords, vocabularies, and dialects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./stable/index.js",
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"./openapi-3-1": "./openapi-3-1/index.js",
|
|
16
16
|
"./experimental": "./lib/experimental.js",
|
|
17
17
|
"./schema/experimental": "./lib/schema.js",
|
|
18
|
-
"./instance/experimental": "./lib/instance.js"
|
|
18
|
+
"./instance/experimental": "./lib/instance.js",
|
|
19
|
+
"./bundle": "./bundle/index.js"
|
|
19
20
|
},
|
|
20
21
|
"browser": {
|
|
21
22
|
"./lib/fetch.js": "./lib/fetch.browser.js"
|
|
@@ -23,7 +24,7 @@
|
|
|
23
24
|
"scripts": {
|
|
24
25
|
"clean": "xargs -a .gitignore rm -rf",
|
|
25
26
|
"lint": "eslint lib stable draft-* openapi-*",
|
|
26
|
-
"test": "mocha 'lib/**/*.spec.ts' 'stable/**/*.spec.ts' 'draft-*/**/*.spec.ts' 'openapi-*/**/*.spec.ts'"
|
|
27
|
+
"test": "mocha 'lib/**/*.spec.ts' 'stable/**/*.spec.ts' 'draft-*/**/*.spec.ts' 'openapi-*/**/*.spec.ts' 'bundle/**/*.spec.ts'"
|
|
27
28
|
},
|
|
28
29
|
"repository": "github:hyperjump-io/json-schema",
|
|
29
30
|
"keywords": [
|
|
@@ -67,6 +68,7 @@
|
|
|
67
68
|
"@hyperjump/uri": "^1.0.0",
|
|
68
69
|
"content-type": "^1.0.4",
|
|
69
70
|
"fastest-stable-stringify": "^2.0.2",
|
|
70
|
-
"node-fetch": "^3.3.0"
|
|
71
|
+
"node-fetch": "^3.3.0",
|
|
72
|
+
"uuid": "^9.0.0"
|
|
71
73
|
}
|
|
72
74
|
}
|