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/src/keywords/not.ts
CHANGED
|
@@ -1,26 +1,39 @@
|
|
|
1
1
|
import { Keyword, JsonSchemaValidatorParams } from "../Keyword";
|
|
2
|
-
import { SchemaNode } from "../types";
|
|
2
|
+
import { isBooleanSchema, isJsonSchema, SchemaNode } from "../types";
|
|
3
3
|
import { validateNode } from "../validateNode";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
const KEYWORD = "not";
|
|
6
|
+
|
|
7
|
+
export const notKeyword: Keyword<"not"> = {
|
|
8
|
+
id: KEYWORD,
|
|
9
|
+
keyword: KEYWORD,
|
|
8
10
|
parse: parseNot,
|
|
9
|
-
addValidate: (node) => node
|
|
11
|
+
addValidate: (node) => node[KEYWORD] != null,
|
|
10
12
|
validate: validateNot
|
|
11
13
|
};
|
|
12
14
|
|
|
13
15
|
export function parseNot(node: SchemaNode) {
|
|
14
16
|
const { schema, evaluationPath, schemaLocation } = node;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
const not = schema[KEYWORD];
|
|
18
|
+
if (not == null) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (!isJsonSchema(not) && !isBooleanSchema(not)) {
|
|
22
|
+
return node.createError("schema-error", {
|
|
23
|
+
pointer: `${schemaLocation}/${KEYWORD}`,
|
|
24
|
+
schema,
|
|
25
|
+
value: not,
|
|
26
|
+
message: `Keyword '${KEYWORD}' must be a valid JSON Schema - received '${typeof not}'`
|
|
27
|
+
});
|
|
17
28
|
}
|
|
29
|
+
node[KEYWORD] = node.compileSchema(schema[KEYWORD], `${evaluationPath}/not`, `${schemaLocation}/not`);
|
|
30
|
+
return node[KEYWORD].schemaValidation;
|
|
18
31
|
}
|
|
19
32
|
|
|
20
|
-
function validateNot({ node, data, pointer, path }: JsonSchemaValidatorParams) {
|
|
33
|
+
function validateNot({ node, data, pointer, path }: JsonSchemaValidatorParams<"not">) {
|
|
21
34
|
const { schema } = node;
|
|
22
35
|
// not has been tested in addValidate
|
|
23
|
-
if (validateNode(node
|
|
24
|
-
return node.createError("not-error", { value: data, not: schema
|
|
36
|
+
if (validateNode(node[KEYWORD]!, data, pointer, path).length === 0) {
|
|
37
|
+
return node.createError("not-error", { value: data, not: schema[KEYWORD], pointer, schema });
|
|
25
38
|
}
|
|
26
39
|
}
|
package/src/keywords/oneOf.ts
CHANGED
|
@@ -3,7 +3,8 @@ import {
|
|
|
3
3
|
JsonSchemaReducerParams,
|
|
4
4
|
JsonSchemaValidatorParams,
|
|
5
5
|
ValidationPath,
|
|
6
|
-
ValidationReturnType
|
|
6
|
+
ValidationReturnType,
|
|
7
|
+
ValidationAnnotation
|
|
7
8
|
} from "../Keyword";
|
|
8
9
|
import { isSchemaNode, SchemaNode } from "../types";
|
|
9
10
|
import settings from "../settings";
|
|
@@ -13,15 +14,16 @@ import { isObject } from "../utils/isObject";
|
|
|
13
14
|
import { validateNode } from "../validateNode";
|
|
14
15
|
import { joinDynamicId } from "../SchemaNode";
|
|
15
16
|
|
|
17
|
+
const KEYWORD = "oneOf";
|
|
16
18
|
const { DECLARATOR_ONEOF } = settings;
|
|
17
19
|
|
|
18
20
|
export const oneOfKeyword: Keyword = {
|
|
19
|
-
id:
|
|
20
|
-
keyword:
|
|
21
|
+
id: KEYWORD,
|
|
22
|
+
keyword: KEYWORD,
|
|
21
23
|
parse: parseOneOf,
|
|
22
|
-
addReduce: (node) => node
|
|
24
|
+
addReduce: (node) => node[KEYWORD] != null,
|
|
23
25
|
reduce: reduceOneOf,
|
|
24
|
-
addValidate: (node) => node
|
|
26
|
+
addValidate: (node) => node[KEYWORD] != null,
|
|
25
27
|
validate: oneOfValidator
|
|
26
28
|
};
|
|
27
29
|
|
|
@@ -37,11 +39,29 @@ export const oneOfFuzzyKeyword: Keyword = {
|
|
|
37
39
|
|
|
38
40
|
export function parseOneOf(node: SchemaNode) {
|
|
39
41
|
const { schema, evaluationPath, schemaLocation } = node;
|
|
40
|
-
if (
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
if (schema[KEYWORD] == null) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (!Array.isArray(schema[KEYWORD])) {
|
|
46
|
+
return node.createError("schema-error", {
|
|
47
|
+
pointer: schemaLocation,
|
|
48
|
+
schema,
|
|
49
|
+
value: schema[KEYWORD],
|
|
50
|
+
message: `Keyword '${KEYWORD}' must be an array - received '${typeof schema[KEYWORD]}'`
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (schema[KEYWORD].length === 0) {
|
|
54
|
+
return;
|
|
44
55
|
}
|
|
56
|
+
|
|
57
|
+
node[KEYWORD] = schema[KEYWORD].map((s, index) =>
|
|
58
|
+
node.compileSchema(s, `${evaluationPath}/${KEYWORD}/${index}`, `${schemaLocation}/${KEYWORD}/${index}`)
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
return node[KEYWORD].reduce((errors, node) => {
|
|
62
|
+
if (node.schemaValidation) errors.push(...node.schemaValidation);
|
|
63
|
+
return errors;
|
|
64
|
+
}, [] as ValidationAnnotation[]);
|
|
45
65
|
}
|
|
46
66
|
|
|
47
67
|
function reduceOneOf({ node, data, pointer, path }: Omit<JsonSchemaReducerParams, "key">) {
|
package/src/keywords/pattern.ts
CHANGED
|
@@ -1,22 +1,49 @@
|
|
|
1
1
|
import { Keyword, JsonSchemaValidatorParams } from "../Keyword";
|
|
2
|
+
import { SchemaNode } from "../SchemaNode";
|
|
2
3
|
import settings from "../settings";
|
|
3
4
|
|
|
5
|
+
const KEYWORD = "pattern";
|
|
4
6
|
const { REGEX_FLAGS } = settings;
|
|
5
7
|
|
|
6
|
-
export const patternKeyword: Keyword = {
|
|
7
|
-
id:
|
|
8
|
-
keyword:
|
|
9
|
-
|
|
8
|
+
export const patternKeyword: Keyword<"pattern"> = {
|
|
9
|
+
id: KEYWORD,
|
|
10
|
+
keyword: KEYWORD,
|
|
11
|
+
parse: parsePattern,
|
|
12
|
+
addValidate: (node) => node[KEYWORD] != null,
|
|
10
13
|
validate: validatePattern
|
|
11
14
|
};
|
|
12
15
|
|
|
13
|
-
function
|
|
14
|
-
const
|
|
16
|
+
function parsePattern(node: SchemaNode) {
|
|
17
|
+
const pattern = node.schema[KEYWORD];
|
|
18
|
+
if (pattern == null) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (typeof pattern !== "string") {
|
|
22
|
+
return node.createError("schema-error", {
|
|
23
|
+
pointer: node.schemaLocation,
|
|
24
|
+
schema: node.schema,
|
|
25
|
+
value: pattern,
|
|
26
|
+
message: `Keyword 'pattern' must be a string - received '${typeof pattern}'`
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
node[KEYWORD] = new RegExp(pattern, node.schema.regexFlags ?? REGEX_FLAGS);
|
|
31
|
+
} catch (e) {
|
|
32
|
+
return node.createError("schema-error", {
|
|
33
|
+
pointer: node.schemaLocation,
|
|
34
|
+
schema: node.schema,
|
|
35
|
+
value: pattern,
|
|
36
|
+
message: (e as Error).message
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function validatePattern({ node, data, pointer = "#" }: JsonSchemaValidatorParams<"pattern">) {
|
|
15
42
|
if (typeof data !== "string") {
|
|
16
43
|
return;
|
|
17
44
|
}
|
|
18
|
-
|
|
19
|
-
|
|
45
|
+
if (node.pattern.test(data) === false) {
|
|
46
|
+
const { schema } = node;
|
|
20
47
|
return node.createError("pattern-error", {
|
|
21
48
|
pattern: schema.pattern,
|
|
22
49
|
description: schema.patternExample || schema.pattern,
|
|
@@ -26,5 +53,4 @@ function validatePattern({ node, data, pointer = "#" }: JsonSchemaValidatorParam
|
|
|
26
53
|
pointer
|
|
27
54
|
});
|
|
28
55
|
}
|
|
29
|
-
return undefined;
|
|
30
56
|
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { getValue } from "../utils/getValue";
|
|
2
|
-
import { SchemaNode } from "../types";
|
|
3
|
-
import {
|
|
2
|
+
import { JsonError, SchemaNode } from "../types";
|
|
3
|
+
import {
|
|
4
|
+
Keyword,
|
|
5
|
+
JsonSchemaResolverParams,
|
|
6
|
+
JsonSchemaValidatorParams,
|
|
7
|
+
ValidationReturnType,
|
|
8
|
+
ValidationAnnotation
|
|
9
|
+
} from "../Keyword";
|
|
4
10
|
import { isObject } from "../utils/isObject";
|
|
5
11
|
import { validateNode } from "../validateNode";
|
|
6
12
|
|
|
@@ -20,18 +26,35 @@ function propertyResolver({ node, key }: JsonSchemaResolverParams) {
|
|
|
20
26
|
|
|
21
27
|
export function parseProperties(node: SchemaNode) {
|
|
22
28
|
const { schema, evaluationPath, schemaLocation } = node;
|
|
23
|
-
if (schema.properties) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
if (schema.properties == null) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (schema.properties && !isObject(schema.properties)) {
|
|
34
|
+
return node.createError("schema-error", {
|
|
35
|
+
pointer: schemaLocation,
|
|
36
|
+
schema,
|
|
37
|
+
value: undefined,
|
|
38
|
+
message: "keyword `properties` must be of type `object`"
|
|
32
39
|
});
|
|
33
|
-
node.properties = parsedProperties;
|
|
34
40
|
}
|
|
41
|
+
|
|
42
|
+
const errors: ValidationAnnotation[] = [];
|
|
43
|
+
const parsedProperties: Record<string, SchemaNode> = {};
|
|
44
|
+
Object.keys(schema.properties).forEach((propertyName) => {
|
|
45
|
+
const propertyNode = node.compileSchema(
|
|
46
|
+
schema.properties[propertyName],
|
|
47
|
+
`${evaluationPath}/properties/${propertyName}`,
|
|
48
|
+
`${schemaLocation}/properties/${propertyName}`
|
|
49
|
+
);
|
|
50
|
+
parsedProperties[propertyName] = propertyNode;
|
|
51
|
+
if (parsedProperties[propertyName].schemaValidation) {
|
|
52
|
+
errors.push(...parsedProperties[propertyName].schemaValidation);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
node.properties = parsedProperties;
|
|
56
|
+
|
|
57
|
+
return errors;
|
|
35
58
|
}
|
|
36
59
|
|
|
37
60
|
function validateProperties({ node, data, pointer, path }: JsonSchemaValidatorParams) {
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { strict as assert } from "assert";
|
|
2
|
+
import { compileSchema } from "../compileSchema";
|
|
3
|
+
import { draft2020 } from "../draft2020";
|
|
4
|
+
import { extendDraft } from "../Draft";
|
|
5
|
+
import { propertyDependenciesKeyword } from "./propertyDependencies";
|
|
6
|
+
import { isSchemaNode } from "../SchemaNode";
|
|
7
|
+
|
|
8
|
+
const drafts = [
|
|
9
|
+
extendDraft(draft2020, {
|
|
10
|
+
keywords: [propertyDependenciesKeyword]
|
|
11
|
+
})
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
describe("keyword : propertyDependencies : validate", () => {
|
|
15
|
+
it("should return error if schema at matching property+value is invalid", () => {
|
|
16
|
+
const node = compileSchema(
|
|
17
|
+
{
|
|
18
|
+
type: "array",
|
|
19
|
+
items: {
|
|
20
|
+
propertyDependencies: {
|
|
21
|
+
propertyName: {
|
|
22
|
+
propertyValue: {
|
|
23
|
+
$ref: "#/$defs/object"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
$defs: {
|
|
29
|
+
object: {
|
|
30
|
+
type: "object",
|
|
31
|
+
required: ["propertyName", "test"],
|
|
32
|
+
properties: {
|
|
33
|
+
propertyName: { type: "string" },
|
|
34
|
+
test: { type: "string" }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
{ drafts }
|
|
40
|
+
);
|
|
41
|
+
const { errors } = node.validate([{ propertyName: "propertyValue", test: 123 }]);
|
|
42
|
+
assert.equal(errors.length, 1);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("should return all errors for schemata at matching property+value", () => {
|
|
46
|
+
const node = compileSchema(
|
|
47
|
+
{
|
|
48
|
+
type: "array",
|
|
49
|
+
items: {
|
|
50
|
+
propertyDependencies: {
|
|
51
|
+
propertyName: {
|
|
52
|
+
propertyValue: {
|
|
53
|
+
$ref: "#/$defs/object"
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
type: {
|
|
57
|
+
headline: {
|
|
58
|
+
$ref: "#/$defs/object"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
$defs: {
|
|
64
|
+
object: {
|
|
65
|
+
type: "object",
|
|
66
|
+
required: ["propertyName", "type", "test"],
|
|
67
|
+
properties: {
|
|
68
|
+
propertyName: { type: "string" },
|
|
69
|
+
type: { type: "string" },
|
|
70
|
+
test: { type: "number" }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
{ drafts }
|
|
76
|
+
);
|
|
77
|
+
const { errors } = node.validate([{ propertyName: "propertyValue", type: "headline", test: "123" }]);
|
|
78
|
+
assert.equal(errors.length, 2);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("should be valid for valid schema matching property+value", () => {
|
|
82
|
+
const node = compileSchema(
|
|
83
|
+
{
|
|
84
|
+
type: "array",
|
|
85
|
+
items: {
|
|
86
|
+
propertyDependencies: {
|
|
87
|
+
propertyName: {
|
|
88
|
+
propertyValue: {
|
|
89
|
+
$ref: "#/$defs/object"
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
$defs: {
|
|
95
|
+
object: {
|
|
96
|
+
type: "object",
|
|
97
|
+
required: ["propertyName", "type", "test"],
|
|
98
|
+
properties: {
|
|
99
|
+
propertyName: { type: "string" },
|
|
100
|
+
type: { type: "string" },
|
|
101
|
+
test: { type: "number" }
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
{ drafts }
|
|
107
|
+
);
|
|
108
|
+
const { errors } = node.validate([{ propertyName: "propertyValue", type: "headline", test: 123 }]);
|
|
109
|
+
assert.equal(errors.length, 0);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("should be valid for valid schema matching property+number", () => {
|
|
113
|
+
const node = compileSchema(
|
|
114
|
+
{
|
|
115
|
+
type: "array",
|
|
116
|
+
items: {
|
|
117
|
+
propertyDependencies: {
|
|
118
|
+
test: {
|
|
119
|
+
"123": {
|
|
120
|
+
$ref: "#/$defs/object"
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
$defs: {
|
|
126
|
+
object: {
|
|
127
|
+
type: "object",
|
|
128
|
+
required: ["propertyName", "type", "test"],
|
|
129
|
+
properties: {
|
|
130
|
+
propertyName: { type: "string" },
|
|
131
|
+
type: { type: "string" },
|
|
132
|
+
test: { type: "number" }
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
{ drafts }
|
|
138
|
+
);
|
|
139
|
+
const { errors } = node.validate([{ propertyName: "propertyValue", type: "headline", test: 123 }]);
|
|
140
|
+
assert.equal(errors.length, 0);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("keyword : propertyDependencies : validate", () => {
|
|
145
|
+
it("should return reduced schema of matching property+value", () => {
|
|
146
|
+
const node = compileSchema(
|
|
147
|
+
{
|
|
148
|
+
type: "array",
|
|
149
|
+
items: {
|
|
150
|
+
properties: {
|
|
151
|
+
id: { type: "string" }
|
|
152
|
+
},
|
|
153
|
+
propertyDependencies: {
|
|
154
|
+
propertyName: {
|
|
155
|
+
propertyValue: {
|
|
156
|
+
$ref: "#/$defs/object"
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
$defs: {
|
|
162
|
+
object: {
|
|
163
|
+
type: "object",
|
|
164
|
+
required: ["propertyName", "test"],
|
|
165
|
+
properties: {
|
|
166
|
+
propertyName: { type: "string" },
|
|
167
|
+
test: { type: "string" }
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
{ drafts }
|
|
173
|
+
);
|
|
174
|
+
const reducedNode = node.getNode("#/0", [{ propertyName: "propertyValue", test: 123 }])?.node;
|
|
175
|
+
assert(isSchemaNode(reducedNode));
|
|
176
|
+
assert(reducedNode.schema.propertyDependencies == null);
|
|
177
|
+
assert(reducedNode.schema.properties.id);
|
|
178
|
+
assert(reducedNode.schema.properties.propertyName);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Keyword,
|
|
3
|
+
JsonSchemaValidatorParams,
|
|
4
|
+
JsonSchemaReducerParams,
|
|
5
|
+
ValidationReturnType,
|
|
6
|
+
ValidationAnnotation
|
|
7
|
+
} from "../Keyword";
|
|
8
|
+
import { isBooleanSchema, isJsonSchema, JsonSchema, SchemaNode } from "../types";
|
|
9
|
+
import { hasProperty } from "../utils/hasProperty";
|
|
10
|
+
import { isObject } from "../utils/isObject";
|
|
11
|
+
import { mergeSchema } from "../utils/mergeSchema";
|
|
12
|
+
import sanitizeErrors from "../utils/sanitizeErrors";
|
|
13
|
+
import { validateNode } from "../validateNode";
|
|
14
|
+
|
|
15
|
+
const KEYWORD = "propertyDependencies";
|
|
16
|
+
|
|
17
|
+
function findMatchingSchemata(node: SchemaNode, data: Record<string, unknown>) {
|
|
18
|
+
const dependentProperties = node[KEYWORD];
|
|
19
|
+
if (dependentProperties == null) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
const dependentPropertyNames = Object.keys(dependentProperties);
|
|
23
|
+
const matchingSchemata: { property: string; value: string; node: SchemaNode }[] = [];
|
|
24
|
+
for (const propertyName of dependentPropertyNames) {
|
|
25
|
+
if (hasProperty(data, propertyName)) {
|
|
26
|
+
const value = data[propertyName];
|
|
27
|
+
if (dependentProperties[propertyName][value as string]) {
|
|
28
|
+
matchingSchemata.push({
|
|
29
|
+
property: propertyName,
|
|
30
|
+
value: `${value}`,
|
|
31
|
+
node: dependentProperties[propertyName][value as string]
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return matchingSchemata;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @experimental `propertyDependencies` to resolve schema by nested name and value
|
|
41
|
+
* @reference https://docs.google.com/presentation/d/1ajXlCQcsjjiMLsluFIILR7sN5aDRBnfqQ9DLbcFbqjI/mobilepresent?slide=id.p
|
|
42
|
+
*
|
|
43
|
+
* - matching schemas are resolved and validiated
|
|
44
|
+
* - multiple matching schemas are resolved and validiated
|
|
45
|
+
* - ignores keyword if no schema is matched
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* {
|
|
49
|
+
* type: "object",
|
|
50
|
+
* propertyDependencies: {
|
|
51
|
+
* propertyName: {
|
|
52
|
+
* propertyValue: { $ref: "#/$defs/schema" }
|
|
53
|
+
* }
|
|
54
|
+
* }
|
|
55
|
+
* }
|
|
56
|
+
*
|
|
57
|
+
* matches
|
|
58
|
+
*
|
|
59
|
+
* {
|
|
60
|
+
* "propertyName": "propertyValue",
|
|
61
|
+
* "otherData": 123
|
|
62
|
+
* } with "#/$defs/schema"
|
|
63
|
+
*/
|
|
64
|
+
export const propertyDependenciesKeyword: Keyword = {
|
|
65
|
+
id: KEYWORD,
|
|
66
|
+
keyword: KEYWORD,
|
|
67
|
+
parse: parsePropertyDependencies,
|
|
68
|
+
addValidate: (node) => node[KEYWORD] != null,
|
|
69
|
+
validate: validatePropertyDependencies,
|
|
70
|
+
addReduce: (node) => node[KEYWORD] != null,
|
|
71
|
+
reduce: reducePropertyDependencies
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
function parsePropertyDependencies(node: SchemaNode) {
|
|
75
|
+
const propertyDependencies = node.schema[KEYWORD];
|
|
76
|
+
if (!isObject(propertyDependencies)) {
|
|
77
|
+
return node.createError("schema-error", {
|
|
78
|
+
pointer: `${node.schemaLocation}/${KEYWORD}`,
|
|
79
|
+
schema: node.schema,
|
|
80
|
+
value: propertyDependencies,
|
|
81
|
+
message: `Keyword '${KEYWORD}' must be an object - received '${typeof propertyDependencies}'`
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
const parsed: Record<string, Record<string, SchemaNode>> = {};
|
|
85
|
+
const errors: ValidationAnnotation[] = [];
|
|
86
|
+
Object.keys(propertyDependencies).map((propertyName) => {
|
|
87
|
+
const values = propertyDependencies[propertyName];
|
|
88
|
+
if (!isObject(values)) {
|
|
89
|
+
errors.push(
|
|
90
|
+
node.createError("schema-error", {
|
|
91
|
+
pointer: `${node.schemaLocation}/${KEYWORD}/${propertyName}`,
|
|
92
|
+
schema: node.schema,
|
|
93
|
+
value: propertyDependencies,
|
|
94
|
+
message: `Keyword '${KEYWORD}[string]' must be an object - received '${typeof propertyDependencies}'`
|
|
95
|
+
})
|
|
96
|
+
);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
Object.keys(values).forEach((value) => {
|
|
100
|
+
const schema = values[value];
|
|
101
|
+
if (!(isJsonSchema(schema) || isBooleanSchema(schema))) {
|
|
102
|
+
errors.push(
|
|
103
|
+
node.createError("schema-error", {
|
|
104
|
+
pointer: `${node.schemaLocation}/${KEYWORD}/${propertyName}/${value}`,
|
|
105
|
+
schema: node.schema,
|
|
106
|
+
value: schema,
|
|
107
|
+
message: `Keyword '${KEYWORD}[string][string]' must be a valid JSON Schema - received '${typeof schema}'`
|
|
108
|
+
})
|
|
109
|
+
);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
parsed[propertyName] = parsed[propertyName] ?? {};
|
|
113
|
+
parsed[propertyName][value] = node.compileSchema(
|
|
114
|
+
schema,
|
|
115
|
+
`${node.evaluationPath}/${KEYWORD}/${propertyName}/${value}`,
|
|
116
|
+
`${node.schemaLocation}/${KEYWORD}/${propertyName}/${value}`
|
|
117
|
+
);
|
|
118
|
+
if (parsed[propertyName][value].schemaValidation) {
|
|
119
|
+
errors.push(...parsed[propertyName][value].schemaValidation);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
node[KEYWORD] = parsed;
|
|
124
|
+
return errors;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function validatePropertyDependencies({ node, data, pointer = "#", path }: JsonSchemaValidatorParams) {
|
|
128
|
+
if (!isObject(data)) {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
const matchingSchemata = findMatchingSchemata(node, data);
|
|
132
|
+
if (matchingSchemata == null || matchingSchemata.length === 0) {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
const errors: ValidationReturnType[] = [];
|
|
136
|
+
for (const match of matchingSchemata) {
|
|
137
|
+
const result = validateNode(match.node, data, pointer, path);
|
|
138
|
+
errors.push(result);
|
|
139
|
+
}
|
|
140
|
+
return sanitizeErrors(errors);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function reducePropertyDependencies({ node, data, key, pointer, path }: JsonSchemaReducerParams) {
|
|
144
|
+
if (!isObject(data)) {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
const matchingSchemata = findMatchingSchemata(node, data);
|
|
148
|
+
if (matchingSchemata == null || matchingSchemata.length === 0) {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let mergedSchema = {};
|
|
153
|
+
let dynamicId = "";
|
|
154
|
+
for (const match of matchingSchemata) {
|
|
155
|
+
const { node: schemaNode } = match.node.reduceNode(data, { key, pointer, path });
|
|
156
|
+
if (schemaNode) {
|
|
157
|
+
const nestedDynamicId = schemaNode.dynamicId?.replace(node.dynamicId, "") ?? "";
|
|
158
|
+
const localDynamicId =
|
|
159
|
+
nestedDynamicId === "" ? `propertyDependencies/${match.property}/${match.value}` : nestedDynamicId;
|
|
160
|
+
dynamicId += `${dynamicId === "" ? "" : ","}${localDynamicId}`;
|
|
161
|
+
|
|
162
|
+
const schema = mergeSchema(match.node.schema, schemaNode.schema);
|
|
163
|
+
mergedSchema = mergeSchema(mergedSchema, schema, "propertyDependencies");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return node.compileSchema(
|
|
168
|
+
mergedSchema,
|
|
169
|
+
`${node.evaluationPath}/${dynamicId}`,
|
|
170
|
+
node.schemaLocation,
|
|
171
|
+
`${node.schemaLocation}(${dynamicId})`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
@@ -1,29 +1,41 @@
|
|
|
1
|
-
import { JsonError } from "../types";
|
|
1
|
+
import { isBooleanSchema, isJsonSchema, JsonError } from "../types";
|
|
2
2
|
import { isObject } from "../utils/isObject";
|
|
3
3
|
import { SchemaNode } from "../types";
|
|
4
4
|
import { Keyword, JsonSchemaValidatorParams } from "../Keyword";
|
|
5
5
|
import { validateNode } from "../validateNode";
|
|
6
6
|
|
|
7
|
+
const KEYWORD = "propertyNames";
|
|
8
|
+
|
|
7
9
|
export const propertyNamesKeyword: Keyword = {
|
|
8
|
-
id:
|
|
9
|
-
keyword:
|
|
10
|
+
id: KEYWORD,
|
|
11
|
+
keyword: KEYWORD,
|
|
10
12
|
parse: parsePropertyNames,
|
|
11
|
-
addValidate: (
|
|
13
|
+
addValidate: (node) => node.schema[KEYWORD] != null,
|
|
12
14
|
validate: validatePropertyNames
|
|
13
15
|
};
|
|
14
16
|
|
|
15
17
|
export function parsePropertyNames(node: SchemaNode) {
|
|
16
|
-
const
|
|
18
|
+
const propertyNames = node.schema[KEYWORD];
|
|
17
19
|
if (propertyNames == null) {
|
|
18
20
|
return;
|
|
19
21
|
}
|
|
20
|
-
if (
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
if (!(isJsonSchema(propertyNames) || isBooleanSchema(propertyNames))) {
|
|
23
|
+
return node.createError("schema-error", {
|
|
24
|
+
pointer: `${node.schemaLocation}/${KEYWORD}`,
|
|
25
|
+
schema: node.schema,
|
|
26
|
+
value: propertyNames,
|
|
27
|
+
message: `Keyword '${KEYWORD}' must be a valid JSON Schema - received '${typeof propertyNames}'`
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
if (isBooleanSchema(propertyNames)) {
|
|
31
|
+
return;
|
|
26
32
|
}
|
|
33
|
+
node.propertyNames = node.compileSchema(
|
|
34
|
+
propertyNames,
|
|
35
|
+
`${node.evaluationPath}/propertyNames`,
|
|
36
|
+
`${node.schemaLocation}/propertyNames`
|
|
37
|
+
);
|
|
38
|
+
return node.schemaValidation;
|
|
27
39
|
}
|
|
28
40
|
|
|
29
41
|
function validatePropertyNames({ node, data, pointer, path }: JsonSchemaValidatorParams) {
|
|
@@ -46,11 +58,11 @@ function validatePropertyNames({ node, data, pointer, path }: JsonSchemaValidato
|
|
|
46
58
|
});
|
|
47
59
|
}
|
|
48
60
|
|
|
49
|
-
if (schema
|
|
61
|
+
if (schema[KEYWORD] === true) {
|
|
50
62
|
return undefined;
|
|
51
63
|
}
|
|
52
64
|
|
|
53
|
-
const propertyNames = node
|
|
65
|
+
const propertyNames = node[KEYWORD];
|
|
54
66
|
if (!isObject(propertyNames)) {
|
|
55
67
|
// ignore invalid schema
|
|
56
68
|
return undefined;
|
|
@@ -59,7 +71,7 @@ function validatePropertyNames({ node, data, pointer, path }: JsonSchemaValidato
|
|
|
59
71
|
const errors: JsonError[] = [];
|
|
60
72
|
const properties = Object.keys(data);
|
|
61
73
|
properties.forEach((prop) => {
|
|
62
|
-
const validationResult = validateNode(propertyNames, prop, `${pointer}
|
|
74
|
+
const validationResult = validateNode(propertyNames, prop, `${pointer}/${prop}`, path);
|
|
63
75
|
if (validationResult.length > 0) {
|
|
64
76
|
errors.push(
|
|
65
77
|
node.createError("invalid-property-name-error", {
|