json-schema-library 11.0.4 → 11.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/CHANGELOG.md +11 -0
- package/README.md +112 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +93 -16
- package/dist/index.d.mts +93 -16
- package/dist/index.mjs +1 -1
- package/dist/jlib.js +3 -3
- package/index.ts +10 -1
- package/package.json +11 -8
- package/src/Draft.ts +1 -1
- package/src/Keyword.ts +36 -10
- package/src/SchemaNode.ts +75 -16
- package/src/compileSchema.ts +53 -4
- package/src/errors/errors.ts +4 -1
- package/src/keywords/$defs.ts +34 -8
- package/src/keywords/$ref.ts +12 -0
- package/src/keywords/additionalProperties.ts +19 -8
- package/src/keywords/allOf.ts +44 -18
- package/src/keywords/anyOf.ts +38 -19
- package/src/keywords/contains.ts +21 -9
- package/src/keywords/dependencies.ts +37 -17
- package/src/keywords/dependentRequired.ts +56 -38
- package/src/keywords/dependentSchemas.ts +37 -13
- package/src/keywords/deprecated.ts +32 -8
- package/src/keywords/enum.ts +30 -8
- package/src/keywords/exclusiveMaximum.ts +21 -2
- package/src/keywords/exclusiveMinimum.ts +22 -3
- package/src/keywords/format.ts +21 -2
- package/src/keywords/ifthenelse.ts +49 -5
- package/src/keywords/items.ts +27 -13
- package/src/keywords/maxItems.ts +22 -2
- package/src/keywords/maxLength.ts +30 -9
- package/src/keywords/maxProperties.ts +30 -9
- package/src/keywords/maximum.ts +28 -8
- package/src/keywords/minItems.ts +30 -9
- package/src/keywords/minLength.ts +30 -9
- package/src/keywords/minProperties.ts +26 -5
- package/src/keywords/minimum.ts +32 -13
- package/src/keywords/multipleOf.ts +33 -12
- package/src/keywords/not.ts +23 -10
- package/src/keywords/oneOf.ts +29 -9
- package/src/keywords/pattern.ts +35 -9
- package/src/keywords/properties.ts +35 -12
- package/src/keywords/propertyDependencies.test.ts +180 -0
- package/src/keywords/propertyDependencies.ts +173 -0
- package/src/keywords/propertyNames.ts +26 -14
- package/src/keywords/required.ts +31 -8
- package/src/keywords/type.ts +53 -16
- package/src/keywords/unevaluatedItems.ts +24 -8
- package/src/keywords/unevaluatedProperties.ts +24 -7
- package/src/keywords/uniqueItems.ts +23 -4
- package/src/mergeNode.ts +9 -4
- package/src/settings.ts +2 -1
- package/src/types.ts +1 -1
- package/src/utils/isListOfStrings.ts +3 -0
- package/src/validate.test.ts +0 -2
- package/src/validateNode.ts +2 -2
- package/src/validateSchema.test.ts +312 -0
package/index.ts
CHANGED
|
@@ -3,7 +3,15 @@ export type { CompileOptions } from "./src/compileSchema";
|
|
|
3
3
|
export type { Context, SchemaNode, GetNodeOptions, ValidateReturnType } from "./src/SchemaNode";
|
|
4
4
|
export type { DataNode } from "./src/methods/toDataNodes";
|
|
5
5
|
export type { Draft, DraftVersion } from "./src/Draft";
|
|
6
|
-
export type {
|
|
6
|
+
export type {
|
|
7
|
+
JsonError,
|
|
8
|
+
JsonAnnotation,
|
|
9
|
+
JsonPointer,
|
|
10
|
+
JsonSchema,
|
|
11
|
+
BooleanSchema,
|
|
12
|
+
OptionalNodeOrError,
|
|
13
|
+
NodeOrError
|
|
14
|
+
} from "./src/types";
|
|
7
15
|
export type {
|
|
8
16
|
Keyword,
|
|
9
17
|
Maybe,
|
|
@@ -29,6 +37,7 @@ export { draftEditor } from "./src/draftEditor";
|
|
|
29
37
|
|
|
30
38
|
// keywords
|
|
31
39
|
export { oneOfFuzzyKeyword, oneOfKeyword } from "./src/keywords/oneOf";
|
|
40
|
+
export { propertyDependenciesKeyword } from "./src/keywords/propertyDependencies";
|
|
32
41
|
|
|
33
42
|
// errors
|
|
34
43
|
export { render } from "./src/errors/render";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "json-schema-library",
|
|
3
|
-
"version": "11.0
|
|
3
|
+
"version": "11.1.0",
|
|
4
4
|
"description": "Customizable and hackable json-validator and json-schema utilities for traversal, data generation and validation",
|
|
5
5
|
"main": "./dist/index.cjs",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
"./package.json": "./package.json"
|
|
14
14
|
},
|
|
15
15
|
"scripts": {
|
|
16
|
-
"preinstall": "npx only-allow yarn",
|
|
17
16
|
"coverage": "nyc npm run test --reporter=lcov",
|
|
18
17
|
"dist": "tsdown -f esm -f cjs --minify; yarn dist:iife",
|
|
19
18
|
"dist:iife": "tsdown --config tsdown.iife.config.ts --no-clean --minify; mv dist/index.iife.js dist/jlib.js",
|
|
@@ -66,19 +65,19 @@
|
|
|
66
65
|
"homepage": "https://github.com/sagold/json-schema-library",
|
|
67
66
|
"devDependencies": {
|
|
68
67
|
"@eslint/js": "^10.0.1",
|
|
69
|
-
"@types/glob": "^
|
|
68
|
+
"@types/glob": "^ 8.1.0",
|
|
70
69
|
"@types/mocha": "^10.0.6",
|
|
71
|
-
"@types/node": "^25.
|
|
70
|
+
"@types/node": "^25.3.3",
|
|
72
71
|
"@types/valid-url": "^1.0.7",
|
|
73
72
|
"eslint": "^9.39.2",
|
|
74
|
-
"glob": "^13.0.
|
|
73
|
+
"glob": "^13.0.6",
|
|
75
74
|
"json-schema-test-suite": "https://github.com/json-schema-org/JSON-Schema-Test-Suite#Test-JSON-Schema-Acceptance-1.029",
|
|
76
75
|
"mocha": "^11.7.5",
|
|
77
76
|
"nyc": "^17.1.0",
|
|
78
|
-
"tsdown": "^0.
|
|
77
|
+
"tsdown": "^0.21.2",
|
|
79
78
|
"tsx": "^4.21.0",
|
|
80
79
|
"typescript": "^5.9.3",
|
|
81
|
-
"typescript-eslint": "^8.
|
|
80
|
+
"typescript-eslint": "^8.57.0",
|
|
82
81
|
"watch": "^1.0.1"
|
|
83
82
|
},
|
|
84
83
|
"dependencies": {
|
|
@@ -93,14 +92,18 @@
|
|
|
93
92
|
"valid-url": "^1.0.9"
|
|
94
93
|
},
|
|
95
94
|
"resolutions": {
|
|
95
|
+
"ajv": ">=6.14.0",
|
|
96
96
|
"braces": ">=3.0.3",
|
|
97
97
|
"cross-spawn": ">=7.0.6",
|
|
98
|
+
"diff": ">=8.0.3",
|
|
98
99
|
"json5": ">=2.2.3",
|
|
99
100
|
"js-yaml": ">=4.1.1",
|
|
100
101
|
"lodash": ">=4",
|
|
101
102
|
"merge": ">=2",
|
|
102
103
|
"micromatch": ">=4.0.8",
|
|
103
|
-
"
|
|
104
|
+
"serialize-javascript": ">=7.0.3",
|
|
105
|
+
"string-width": ">=4.2.3",
|
|
106
|
+
"minimatch": ">=9.0.7"
|
|
104
107
|
},
|
|
105
108
|
"publishConfig": {
|
|
106
109
|
"registry": "https://registry.npmjs.org"
|
package/src/Draft.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { JsonSchemaValidator, Keyword } from "./Keyword";
|
|
|
2
2
|
import { copyDraft } from "./utils/copyDraft";
|
|
3
3
|
import { createSchema } from "./methods/createSchema";
|
|
4
4
|
import { toDataNodes } from "./methods/toDataNodes";
|
|
5
|
-
import { ErrorConfig } from "./types";
|
|
5
|
+
import { ErrorConfig, SchemaNode } from "./types";
|
|
6
6
|
import { getChildSelection } from "./methods/getChildSelection";
|
|
7
7
|
import { getData } from "./methods/getData";
|
|
8
8
|
|
package/src/Keyword.ts
CHANGED
|
@@ -37,30 +37,56 @@ export type ValidationAnnotation = JsonError | JsonAnnotation | Promise<Maybe<Va
|
|
|
37
37
|
type ValidationResult = Maybe<ValidationAnnotation>;
|
|
38
38
|
export type ValidationReturnType = ValidationResult | ValidationResult[];
|
|
39
39
|
|
|
40
|
-
export type
|
|
41
|
-
export
|
|
40
|
+
export type SchemaNodeWithRequired<K extends keyof SchemaNode> = SchemaNode & Required<Pick<SchemaNode, K>>;
|
|
41
|
+
export type JsonSchemaValidatorParams<Key extends keyof SchemaNode = keyof SchemaNode> = {
|
|
42
|
+
pointer: string;
|
|
43
|
+
data: unknown;
|
|
44
|
+
node: SchemaNodeWithRequired<Key>;
|
|
45
|
+
path: ValidationPath;
|
|
46
|
+
};
|
|
47
|
+
export interface JsonSchemaValidator<Key extends keyof SchemaNode = keyof SchemaNode> {
|
|
42
48
|
toJSON?: () => string;
|
|
43
49
|
order?: number;
|
|
44
|
-
(options: JsonSchemaValidatorParams): ValidationReturnType;
|
|
50
|
+
(options: JsonSchemaValidatorParams<Key>): ValidationReturnType;
|
|
45
51
|
}
|
|
46
52
|
|
|
47
|
-
export type Keyword = {
|
|
53
|
+
export type Keyword<Key extends keyof SchemaNode = keyof SchemaNode> = {
|
|
48
54
|
id: string;
|
|
49
55
|
/** unique keyword corresponding to JSON Schema keywords (or custom) */
|
|
50
56
|
keyword: string;
|
|
51
57
|
/** sort order of keyword. Lower numbers will be processed last. Default is 0 */
|
|
52
58
|
order?: number;
|
|
53
|
-
/**
|
|
54
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Called once for each JSON Schema dduring compileSchema to evaluate keyword.
|
|
61
|
+
* Use this to skip or preprocess the Keyword for the given JSON Schema and
|
|
62
|
+
* to create any schema annotations, like input errors.
|
|
63
|
+
*
|
|
64
|
+
* - most keywords cache their evaluation directly on node, e.g. node.required
|
|
65
|
+
* - most keywords skip any other actions if their evaluation is missing on node
|
|
66
|
+
* - return any errors found in JSON schema related to this keyword
|
|
67
|
+
* (this includes errors from created nodes)
|
|
68
|
+
*/
|
|
69
|
+
parse?: (node: SchemaNode) => void | ValidationAnnotation | ValidationAnnotation[];
|
|
55
70
|
addResolve?: (node: SchemaNode) => boolean;
|
|
56
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* If this contains child-data, resolve must return schema associated for the passed in key
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* a keyword properties has has child-properties. So when a properties[key] exists,
|
|
76
|
+
* it will return the node of properties[key] or nothing at all
|
|
77
|
+
*/
|
|
57
78
|
resolve?: JsonSchemaResolver;
|
|
58
79
|
|
|
80
|
+
/** return true if the given node should run the validate-function on this keyword */
|
|
59
81
|
addValidate?: (node: SchemaNode) => boolean;
|
|
60
|
-
/**
|
|
61
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Perform validation for this keyword and the passed in data
|
|
84
|
+
*/
|
|
85
|
+
validate?: JsonSchemaValidator<Key>;
|
|
62
86
|
|
|
63
87
|
addReduce?: (node: SchemaNode) => boolean;
|
|
64
|
-
/**
|
|
88
|
+
/**
|
|
89
|
+
* Remove dynamic schema-keywords by merging valid sub-schemas
|
|
90
|
+
*/
|
|
65
91
|
reduce?: JsonSchemaReducer;
|
|
66
92
|
};
|
package/src/SchemaNode.ts
CHANGED
|
@@ -24,7 +24,8 @@ import {
|
|
|
24
24
|
OptionalNodeOrError,
|
|
25
25
|
NodeOrError,
|
|
26
26
|
JsonAnnotation,
|
|
27
|
-
isJsonAnnotation
|
|
27
|
+
isJsonAnnotation,
|
|
28
|
+
isBooleanSchema
|
|
28
29
|
} from "./types";
|
|
29
30
|
import { isObject } from "./utils/isObject";
|
|
30
31
|
import { join } from "@sagold/json-pointer";
|
|
@@ -91,6 +92,8 @@ export type Context = {
|
|
|
91
92
|
formats: Draft["formats"];
|
|
92
93
|
/** [SHARED USING ADD REMOTE] getData default options */
|
|
93
94
|
getDataDefaultOptions?: TemplateOptions;
|
|
95
|
+
/** [SHARED USING ADD REMOTE] collect unknown keywords in schemaAnnotations */
|
|
96
|
+
withSchemaAnnotations?: boolean;
|
|
94
97
|
};
|
|
95
98
|
|
|
96
99
|
export interface SchemaNode extends SchemaNodeMethodsType {
|
|
@@ -129,6 +132,7 @@ export interface SchemaNode extends SchemaNodeMethodsType {
|
|
|
129
132
|
reducers: JsonSchemaReducer[];
|
|
130
133
|
resolvers: JsonSchemaResolver[];
|
|
131
134
|
validators: JsonSchemaValidator[];
|
|
135
|
+
schemaValidation?: ValidationAnnotation[];
|
|
132
136
|
|
|
133
137
|
// parsed schema properties (registered by parsers)
|
|
134
138
|
$id?: string;
|
|
@@ -140,7 +144,9 @@ export interface SchemaNode extends SchemaNodeMethodsType {
|
|
|
140
144
|
contains?: SchemaNode;
|
|
141
145
|
dependentRequired?: Record<string, string[]>;
|
|
142
146
|
dependentSchemas?: Record<string, SchemaNode | boolean>;
|
|
147
|
+
deprecated?: boolean;
|
|
143
148
|
else?: SchemaNode;
|
|
149
|
+
enum?: unknown[];
|
|
144
150
|
if?: SchemaNode;
|
|
145
151
|
/**
|
|
146
152
|
* # Items-array schema - for all drafts
|
|
@@ -177,21 +183,40 @@ export interface SchemaNode extends SchemaNodeMethodsType {
|
|
|
177
183
|
* | [AdditionalItems Specification](https://json-schema.org/draft/2019-09/draft-handrews-json-schema-02#additionalItems)
|
|
178
184
|
*/
|
|
179
185
|
items?: SchemaNode;
|
|
186
|
+
maximum?: number;
|
|
187
|
+
minimum?: number;
|
|
188
|
+
maxItems?: number;
|
|
189
|
+
maxLength?: number;
|
|
190
|
+
maxProperties?: number;
|
|
191
|
+
minItems?: number;
|
|
192
|
+
minLength?: number;
|
|
193
|
+
minProperties?: number;
|
|
180
194
|
not?: SchemaNode;
|
|
181
195
|
oneOf?: SchemaNode[];
|
|
196
|
+
multipleOf?: number;
|
|
197
|
+
pattern?: RegExp;
|
|
182
198
|
patternProperties?: { name: string; pattern: RegExp; node: SchemaNode }[];
|
|
199
|
+
propertyDependencies?: Record<string, Record<string, SchemaNode>>;
|
|
183
200
|
properties?: Record<string, SchemaNode>;
|
|
184
201
|
propertyNames?: SchemaNode;
|
|
202
|
+
required?: string[];
|
|
185
203
|
then?: SchemaNode;
|
|
204
|
+
type?: string | string[];
|
|
186
205
|
unevaluatedItems?: SchemaNode;
|
|
187
206
|
unevaluatedProperties?: SchemaNode;
|
|
207
|
+
uniqueItems?: true;
|
|
188
208
|
}
|
|
189
209
|
|
|
190
210
|
/**
|
|
191
211
|
* Fixed SchemaNode mixin methods
|
|
192
212
|
*/
|
|
193
213
|
interface SchemaNodeMethodsType {
|
|
194
|
-
compileSchema(
|
|
214
|
+
compileSchema(
|
|
215
|
+
schema: JsonSchema | BooleanSchema,
|
|
216
|
+
evaluationPath?: string,
|
|
217
|
+
schemaLocation?: string,
|
|
218
|
+
dynamicId?: string
|
|
219
|
+
): SchemaNode;
|
|
195
220
|
createError<T extends string = DefaultErrors>(code: T, data: AnnotationData, message?: string): JsonError;
|
|
196
221
|
createAnnotation<T extends string = DefaultErrors>(code: T, data: AnnotationData, message?: string): JsonAnnotation;
|
|
197
222
|
createSchema(data?: unknown): JsonSchema;
|
|
@@ -325,7 +350,20 @@ export const SchemaNodeMethods = {
|
|
|
325
350
|
...SchemaNodeMethods
|
|
326
351
|
};
|
|
327
352
|
|
|
328
|
-
|
|
353
|
+
if (!isJsonSchema(schema) && !isBooleanSchema(schema)) {
|
|
354
|
+
node.schemaValidation = [
|
|
355
|
+
node.createError("schema-error", {
|
|
356
|
+
pointer: schemaLocation ?? evaluationPath,
|
|
357
|
+
schema,
|
|
358
|
+
value: undefined,
|
|
359
|
+
message: `JSON schema must be object or boolean - reveived: '${schema}'`
|
|
360
|
+
})
|
|
361
|
+
];
|
|
362
|
+
return node;
|
|
363
|
+
}
|
|
364
|
+
const schemaValidation = addKeywords(node).filter((err) => err != null);
|
|
365
|
+
node.schemaValidation = sanitizeErrors(schemaValidation);
|
|
366
|
+
|
|
329
367
|
return node;
|
|
330
368
|
},
|
|
331
369
|
|
|
@@ -497,15 +535,15 @@ export const SchemaNodeMethods = {
|
|
|
497
535
|
* @returns the current node (not the remote schema-node)
|
|
498
536
|
*/
|
|
499
537
|
addRemoteSchema(url: string, schema: JsonSchema | BooleanSchema): SchemaNode {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
538
|
+
// @draft >= 6
|
|
539
|
+
if (isJsonSchema(schema)) {
|
|
540
|
+
schema.$id = resolveUri(schema.$id || url);
|
|
541
|
+
}
|
|
542
|
+
|
|
505
543
|
const node = this as SchemaNode;
|
|
506
544
|
const { context } = node;
|
|
507
545
|
const schemaId = isJsonSchema(schema) ? schema.$schema : undefined;
|
|
508
|
-
const draft = getDraft(context.drafts, schemaId ?? context.rootNode.schema?.$schema);
|
|
546
|
+
const draft = getDraft(context.drafts, schemaId ?? context.rootNode.schema?.$schema);
|
|
509
547
|
|
|
510
548
|
const remoteNode: SchemaNode = {
|
|
511
549
|
evaluationPath: "#",
|
|
@@ -565,20 +603,40 @@ const noRefMergeDrafts = ["draft-04", "draft-06", "draft-07"];
|
|
|
565
603
|
export function addKeywords(node: SchemaNode) {
|
|
566
604
|
if (node.schema.$ref && noRefMergeDrafts.includes(node.context.version)) {
|
|
567
605
|
// for these draft versions only ref is validated
|
|
568
|
-
node.context.keywords
|
|
606
|
+
return node.context.keywords
|
|
569
607
|
.filter(({ keyword }) => whitelist.includes(keyword))
|
|
570
|
-
.
|
|
571
|
-
return;
|
|
608
|
+
.map((keyword) => execKeyword(keyword, node));
|
|
572
609
|
}
|
|
573
610
|
const keys = Object.keys(node.schema);
|
|
574
|
-
node.context.keywords
|
|
575
|
-
.filter(({ keyword }) =>
|
|
576
|
-
.
|
|
611
|
+
const errors = node.context.keywords
|
|
612
|
+
.filter(({ keyword }) => whitelist.includes(keyword) || keys.includes(keyword))
|
|
613
|
+
.map((keyword) => execKeyword(keyword, node));
|
|
614
|
+
|
|
615
|
+
// find unused keywords
|
|
616
|
+
if (node.context.withSchemaAnnotations) {
|
|
617
|
+
Object.keys(node.schema)
|
|
618
|
+
.filter(
|
|
619
|
+
(key) =>
|
|
620
|
+
!key.startsWith("x-") && node.context.keywords.find((keyword) => keyword.keyword === key) == null
|
|
621
|
+
)
|
|
622
|
+
.forEach((keyword) => {
|
|
623
|
+
errors.push(
|
|
624
|
+
node.createAnnotation("unknown-keyword-warning", {
|
|
625
|
+
pointer: `${node.schemaLocation}/${keyword}`,
|
|
626
|
+
schema: node.schema,
|
|
627
|
+
value: keyword,
|
|
628
|
+
draft: node.getDraftVersion()
|
|
629
|
+
})
|
|
630
|
+
);
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return errors;
|
|
577
635
|
}
|
|
578
636
|
|
|
579
637
|
export function execKeyword(keyword: Keyword, node: SchemaNode) {
|
|
580
638
|
// @todo consider first parsing all nodes
|
|
581
|
-
keyword.parse?.(node);
|
|
639
|
+
const errors = keyword.parse?.(node);
|
|
582
640
|
if (keyword.reduce && keyword.addReduce?.(node)) {
|
|
583
641
|
node.reducers.push(keyword.reduce);
|
|
584
642
|
}
|
|
@@ -588,4 +646,5 @@ export function execKeyword(keyword: Keyword, node: SchemaNode) {
|
|
|
588
646
|
if (keyword.validate && keyword.addValidate?.(node)) {
|
|
589
647
|
node.validators.push(keyword.validate);
|
|
590
648
|
}
|
|
649
|
+
return errors;
|
|
591
650
|
}
|
package/src/compileSchema.ts
CHANGED
|
@@ -6,10 +6,21 @@ import { draft07 } from "./draft07";
|
|
|
6
6
|
import { draft2019 } from "./draft2019";
|
|
7
7
|
import { draft2020 } from "./draft2020";
|
|
8
8
|
import { pick } from "./utils/pick";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
JsonSchema,
|
|
11
|
+
BooleanSchema,
|
|
12
|
+
Draft,
|
|
13
|
+
isJsonSchema,
|
|
14
|
+
JsonAnnotation,
|
|
15
|
+
JsonError,
|
|
16
|
+
isJsonError,
|
|
17
|
+
isJsonAnnotation,
|
|
18
|
+
isBooleanSchema
|
|
19
|
+
} from "./types";
|
|
10
20
|
import { TemplateOptions } from "./methods/getData";
|
|
11
21
|
import { SchemaNode, SchemaNodeMethods, addKeywords, isSchemaNode } from "./SchemaNode";
|
|
12
22
|
import settings from "./settings";
|
|
23
|
+
import sanitizeErrors from "./utils/sanitizeErrors";
|
|
13
24
|
|
|
14
25
|
const { REGEX_FLAGS } = settings;
|
|
15
26
|
|
|
@@ -18,6 +29,10 @@ export type CompileOptions = {
|
|
|
18
29
|
remote?: SchemaNode;
|
|
19
30
|
formatAssertion?: boolean | "meta-schema" | undefined;
|
|
20
31
|
getDataDefaultOptions?: TemplateOptions;
|
|
32
|
+
/** set to true to throw an Errors on errors in input schema. Defaults to false */
|
|
33
|
+
throwOnInvalidSchema?: boolean;
|
|
34
|
+
/** set to true to collect unknown keywords of input schema in `node.schemaAnnotations`. Defaults to false */
|
|
35
|
+
withSchemaAnnotations?: boolean;
|
|
21
36
|
};
|
|
22
37
|
|
|
23
38
|
const defaultDrafts: Draft[] = [draft04, draft06, draft07, draft2019, draft2020];
|
|
@@ -35,8 +50,7 @@ export function compileSchema(schema: JsonSchema | BooleanSchema, options: Compi
|
|
|
35
50
|
let formatAssertion = options.formatAssertion ?? true;
|
|
36
51
|
const drafts = options.drafts ?? defaultDrafts;
|
|
37
52
|
const draft = getDraft(drafts, isJsonSchema(schema) ? schema.$schema : undefined);
|
|
38
|
-
|
|
39
|
-
const node: SchemaNode = {
|
|
53
|
+
const node: SchemaNode & { schemaErrors?: JsonError[]; schemaAnnotations: JsonAnnotation[] } = {
|
|
40
54
|
evaluationPath: "#",
|
|
41
55
|
lastIdPointer: "#",
|
|
42
56
|
schemaLocation: "#",
|
|
@@ -54,6 +68,7 @@ export function compileSchema(schema: JsonSchema | BooleanSchema, options: Compi
|
|
|
54
68
|
refs: {},
|
|
55
69
|
...copy(pick(draft, "methods", "keywords", "version", "formats", "errors")),
|
|
56
70
|
getDataDefaultOptions: options.getDataDefaultOptions,
|
|
71
|
+
withSchemaAnnotations: options.withSchemaAnnotations ?? false,
|
|
57
72
|
drafts
|
|
58
73
|
},
|
|
59
74
|
...SchemaNodeMethods
|
|
@@ -82,6 +97,40 @@ export function compileSchema(schema: JsonSchema | BooleanSchema, options: Compi
|
|
|
82
97
|
node.context.keywords = node.context.keywords.filter((f) => f.keyword !== "format");
|
|
83
98
|
}
|
|
84
99
|
|
|
85
|
-
|
|
100
|
+
if (!isJsonSchema(schema) && !isBooleanSchema(schema)) {
|
|
101
|
+
node.schemaErrors = [
|
|
102
|
+
node.createError("schema-error", {
|
|
103
|
+
pointer: "#",
|
|
104
|
+
schema,
|
|
105
|
+
value: undefined,
|
|
106
|
+
message: `JSON schema must be object or boolean - reveived: '${schema}'`
|
|
107
|
+
})
|
|
108
|
+
];
|
|
109
|
+
return node;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// parse and validate schema
|
|
113
|
+
let schemaValidation = addKeywords(node).filter((err) => err != null);
|
|
114
|
+
schemaValidation = sanitizeErrors(schemaValidation);
|
|
115
|
+
const schemaErrors: JsonError[] = [];
|
|
116
|
+
const schemaAnnotations: JsonAnnotation[] = [];
|
|
117
|
+
schemaValidation.forEach((error) => {
|
|
118
|
+
if (isJsonError(error)) {
|
|
119
|
+
schemaErrors.push(error);
|
|
120
|
+
} else if (isJsonAnnotation(error)) {
|
|
121
|
+
schemaAnnotations.push(error);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
if (options.throwOnInvalidSchema && schemaErrors.length > 0) {
|
|
126
|
+
const error = new Error("Invalid schema passed to compileSchema");
|
|
127
|
+
// @ts-expect-error unknown error-property
|
|
128
|
+
error.data = schemaErrors;
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
node.schemaErrors = schemaErrors;
|
|
133
|
+
node.schemaAnnotations = schemaAnnotations;
|
|
134
|
+
|
|
86
135
|
return node;
|
|
87
136
|
}
|
package/src/errors/errors.ts
CHANGED
|
@@ -74,5 +74,8 @@ export const errors = {
|
|
|
74
74
|
"unknown-property-error": "Could not find a valid schema for property `{{pointer}}` within object",
|
|
75
75
|
"value-not-empty-error": "A value for `{{property}}` is required at `{{pointer}}`",
|
|
76
76
|
// annotations
|
|
77
|
-
"deprecated-warning": "Value at `{{pointer}}` is deprecated"
|
|
77
|
+
"deprecated-warning": "Value at `{{pointer}}` is deprecated",
|
|
78
|
+
// schema validation
|
|
79
|
+
"schema-error": "Invalid schema found at {{pointer}}: {{message}}",
|
|
80
|
+
"unknown-keyword-warning": "Keyword '{{value}}' is not a valid keyword to draft '{{draft}}'"
|
|
78
81
|
};
|
package/src/keywords/$defs.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { Keyword } from "../Keyword";
|
|
1
|
+
import { Keyword, ValidationAnnotation } from "../Keyword";
|
|
2
2
|
import { SchemaNode } from "../types";
|
|
3
|
+
import { isObject } from "../utils/isObject";
|
|
3
4
|
|
|
4
5
|
export const $defsKeyword: Keyword = {
|
|
5
6
|
id: "$defs",
|
|
@@ -8,17 +9,40 @@ export const $defsKeyword: Keyword = {
|
|
|
8
9
|
};
|
|
9
10
|
|
|
10
11
|
export function parseDefs(node: SchemaNode) {
|
|
12
|
+
const errors: ValidationAnnotation[] = [];
|
|
13
|
+
|
|
11
14
|
if (node.schema.$defs) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
if (!isObject(node.schema.$defs)) {
|
|
16
|
+
errors.push(
|
|
17
|
+
node.createError("schema-error", {
|
|
18
|
+
pointer: node.schemaLocation,
|
|
19
|
+
schema: node.schema,
|
|
20
|
+
value: node.schema.$defs,
|
|
21
|
+
message: `$defs must be an object - received: ${typeof node.schema.$defs}`
|
|
22
|
+
})
|
|
18
23
|
);
|
|
19
|
-
}
|
|
24
|
+
} else {
|
|
25
|
+
node.$defs = node.$defs ?? {};
|
|
26
|
+
Object.keys(node.schema.$defs).forEach((property) => {
|
|
27
|
+
node.$defs![property] = node.compileSchema(
|
|
28
|
+
node.schema.$defs[property],
|
|
29
|
+
`${node.evaluationPath}/$defs/${urlEncodeJsonPointerProperty(property)}`,
|
|
30
|
+
`${node.schemaLocation}/$defs/${property}`
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
20
34
|
}
|
|
21
35
|
if (node.schema.definitions) {
|
|
36
|
+
if (!isObject(node.schema.definitions)) {
|
|
37
|
+
errors.push(
|
|
38
|
+
node.createError("schema-error", {
|
|
39
|
+
pointer: node.schemaLocation,
|
|
40
|
+
schema: node.schema,
|
|
41
|
+
value: node.schema.$defs,
|
|
42
|
+
message: `definitions must be an object - received: ${typeof node.schema.definitions}`
|
|
43
|
+
})
|
|
44
|
+
);
|
|
45
|
+
}
|
|
22
46
|
node.$defs = node.$defs ?? {};
|
|
23
47
|
Object.keys(node.schema.definitions).forEach((property) => {
|
|
24
48
|
node.$defs![property] = node.compileSchema(
|
|
@@ -28,6 +52,8 @@ export function parseDefs(node: SchemaNode) {
|
|
|
28
52
|
);
|
|
29
53
|
});
|
|
30
54
|
}
|
|
55
|
+
|
|
56
|
+
return errors;
|
|
31
57
|
}
|
|
32
58
|
|
|
33
59
|
function urlEncodeJsonPointerProperty(property: string) {
|
package/src/keywords/$ref.ts
CHANGED
|
@@ -72,6 +72,18 @@ export function parseRef(node: SchemaNode) {
|
|
|
72
72
|
node.$ref = `#${node.$ref}`;
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
|
+
|
|
76
|
+
// validate simple ref to definitions
|
|
77
|
+
if (node.$ref?.startsWith("#/$defs/")) {
|
|
78
|
+
if (get(node.getNodeRoot().schema, node.$ref) == null) {
|
|
79
|
+
return node.createError("schema-error", {
|
|
80
|
+
pointer: `${node.schemaLocation}/$ref`,
|
|
81
|
+
schema: node.schema,
|
|
82
|
+
value: node.schema.$ref,
|
|
83
|
+
message: `Invalid $ref to missing target '${node.schema.ref}'`
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
75
87
|
}
|
|
76
88
|
|
|
77
89
|
export function reduceRef({ node, data, key, pointer, path }: JsonSchemaReducerParams) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import settings from "../settings";
|
|
2
2
|
import { isObject } from "../utils/isObject";
|
|
3
|
-
import { Keyword, JsonSchemaResolverParams, JsonSchemaValidatorParams, ValidationReturnType} from "../Keyword";
|
|
4
|
-
import { SchemaNode } from "../types";
|
|
3
|
+
import { Keyword, JsonSchemaResolverParams, JsonSchemaValidatorParams, ValidationReturnType } from "../Keyword";
|
|
4
|
+
import { isBooleanSchema, SchemaNode } from "../types";
|
|
5
5
|
import { getValue } from "../utils/getValue";
|
|
6
6
|
import { validateNode } from "../validateNode";
|
|
7
7
|
|
|
@@ -24,13 +24,24 @@ export const additionalPropertiesKeyword: Keyword = {
|
|
|
24
24
|
// must come as last resolver
|
|
25
25
|
export function parseAdditionalProperties(node: SchemaNode) {
|
|
26
26
|
const { schema, evaluationPath, schemaLocation } = node;
|
|
27
|
-
if (
|
|
28
|
-
|
|
29
|
-
schema.additionalProperties,
|
|
30
|
-
`${evaluationPath}/additionalProperties`,
|
|
31
|
-
`${schemaLocation}/additionalProperties`
|
|
32
|
-
);
|
|
27
|
+
if (schema.additionalProperties == null || isBooleanSchema(schema.additionalProperties)) {
|
|
28
|
+
return;
|
|
33
29
|
}
|
|
30
|
+
|
|
31
|
+
if (!isObject(schema.additionalProperties)) {
|
|
32
|
+
return node.createError("schema-error", {
|
|
33
|
+
pointer: node.schemaLocation,
|
|
34
|
+
schema,
|
|
35
|
+
value: schema.additionalProperties,
|
|
36
|
+
message: `keyword 'additionalProperties' must be a valid JSON Schema - receoved: ${typeof schema.additionalProperties}`
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
node.additionalProperties = node.compileSchema(
|
|
41
|
+
schema.additionalProperties,
|
|
42
|
+
`${evaluationPath}/additionalProperties`,
|
|
43
|
+
`${schemaLocation}/additionalProperties`
|
|
44
|
+
);
|
|
34
45
|
}
|
|
35
46
|
|
|
36
47
|
function additionalPropertyResolver({ node, data, key }: JsonSchemaResolverParams) {
|
package/src/keywords/allOf.ts
CHANGED
|
@@ -1,29 +1,55 @@
|
|
|
1
1
|
import { mergeSchema } from "../utils/mergeSchema";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
Keyword,
|
|
4
|
+
JsonSchemaReducerParams,
|
|
5
|
+
JsonSchemaValidatorParams,
|
|
6
|
+
ValidationReturnType,
|
|
7
|
+
ValidationAnnotation
|
|
8
|
+
} from "../Keyword";
|
|
3
9
|
import { SchemaNode } from "../types";
|
|
4
10
|
import { validateNode } from "../validateNode";
|
|
5
11
|
|
|
12
|
+
const KEYWORD = "allOf";
|
|
13
|
+
|
|
6
14
|
export const allOfKeyword: Keyword = {
|
|
7
|
-
id:
|
|
8
|
-
keyword:
|
|
15
|
+
id: KEYWORD,
|
|
16
|
+
keyword: KEYWORD,
|
|
9
17
|
parse: parseAllOf,
|
|
10
|
-
addReduce: (node: SchemaNode) => node
|
|
18
|
+
addReduce: (node: SchemaNode) => node[KEYWORD] != null,
|
|
11
19
|
reduce: reduceAllOf,
|
|
12
|
-
addValidate: (node) => node
|
|
20
|
+
addValidate: (node) => node[KEYWORD] != null,
|
|
13
21
|
validate: validateAllOf
|
|
14
22
|
};
|
|
15
23
|
|
|
16
24
|
export function parseAllOf(node: SchemaNode) {
|
|
17
|
-
const { schema, evaluationPath } = node;
|
|
18
|
-
if (
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
25
|
+
const { schema, evaluationPath, schemaLocation } = node;
|
|
26
|
+
if (schema[KEYWORD] == null) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (!Array.isArray(schema[KEYWORD])) {
|
|
30
|
+
return node.createError("schema-error", {
|
|
31
|
+
pointer: schemaLocation,
|
|
32
|
+
schema,
|
|
33
|
+
value: schema[KEYWORD],
|
|
34
|
+
message: `Keyword '${KEYWORD}' must be an array - received '${typeof schema[KEYWORD]}'`
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
if (schema[KEYWORD].length === 0) {
|
|
38
|
+
return;
|
|
22
39
|
}
|
|
40
|
+
|
|
41
|
+
node[KEYWORD] = schema[KEYWORD].map((s, index) =>
|
|
42
|
+
node.compileSchema(s, `${evaluationPath}/${KEYWORD}/${index}`, `${schemaLocation}/${KEYWORD}/${index}`)
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
return node[KEYWORD].reduce((errors, node) => {
|
|
46
|
+
if (node.schemaValidation) errors.push(...node.schemaValidation);
|
|
47
|
+
return errors;
|
|
48
|
+
}, [] as ValidationAnnotation[]);
|
|
23
49
|
}
|
|
24
50
|
|
|
25
51
|
function reduceAllOf({ node, data, key, pointer, path }: JsonSchemaReducerParams) {
|
|
26
|
-
if (node
|
|
52
|
+
if (node[KEYWORD] == null) {
|
|
27
53
|
return;
|
|
28
54
|
}
|
|
29
55
|
|
|
@@ -31,15 +57,15 @@ function reduceAllOf({ node, data, key, pointer, path }: JsonSchemaReducerParams
|
|
|
31
57
|
// dynamic schema parts
|
|
32
58
|
let mergedSchema = {};
|
|
33
59
|
let dynamicId = "";
|
|
34
|
-
for (let i = 0; i < node.
|
|
35
|
-
const { node: schemaNode } = node
|
|
60
|
+
for (let i = 0; i < node[KEYWORD].length; i += 1) {
|
|
61
|
+
const { node: schemaNode } = node[KEYWORD][i].reduceNode(data, { key, pointer, path });
|
|
36
62
|
if (schemaNode) {
|
|
37
63
|
const nestedDynamicId = schemaNode.dynamicId?.replace(node.dynamicId, "") ?? "";
|
|
38
|
-
const localDynamicId = nestedDynamicId === "" ?
|
|
64
|
+
const localDynamicId = nestedDynamicId === "" ? `${KEYWORD}/${i}` : nestedDynamicId;
|
|
39
65
|
dynamicId += `${dynamicId === "" ? "" : ","}${localDynamicId}`;
|
|
40
66
|
|
|
41
|
-
const schema = mergeSchema(node
|
|
42
|
-
mergedSchema = mergeSchema(mergedSchema, schema,
|
|
67
|
+
const schema = mergeSchema(node[KEYWORD][i].schema, schemaNode.schema);
|
|
68
|
+
mergedSchema = mergeSchema(mergedSchema, schema, KEYWORD, "contains");
|
|
43
69
|
}
|
|
44
70
|
}
|
|
45
71
|
|
|
@@ -52,11 +78,11 @@ function reduceAllOf({ node, data, key, pointer, path }: JsonSchemaReducerParams
|
|
|
52
78
|
}
|
|
53
79
|
|
|
54
80
|
function validateAllOf({ node, data, pointer, path }: JsonSchemaValidatorParams) {
|
|
55
|
-
if (!Array.isArray(node
|
|
81
|
+
if (!Array.isArray(node[KEYWORD]) || node[KEYWORD].length === 0) {
|
|
56
82
|
return;
|
|
57
83
|
}
|
|
58
84
|
const errors: ValidationReturnType = [];
|
|
59
|
-
node.
|
|
85
|
+
node[KEYWORD].forEach((allOfNode) => {
|
|
60
86
|
errors.push(...validateNode(allOfNode, data, pointer, path));
|
|
61
87
|
});
|
|
62
88
|
return errors;
|