json-schema-library 11.5.1 → 11.6.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 +7 -0
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -1
- package/dist/index.d.mts +4 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/jlib.js +1 -1
- package/package.json +1 -1
- package/src/SchemaNode.ts +18 -2
- package/src/compileSchema.test.ts +37 -16
- package/src/compileSchema.ts +9 -1
- package/src/keywords/allOf.ts +1 -7
- package/src/keywords/oneOf.test.ts +11 -0
- package/src/keywords/oneOf.ts +32 -0
- package/src/keywords/propertyDependencies.test.ts +21 -0
- package/src/keywords/propertyDependencies.ts +7 -6
- package/src/methods/getChildSelection.test.ts +24 -1
- package/src/methods/getChildSelection.ts +8 -3
- package/src/validateSchema.test.ts +12 -0
package/src/SchemaNode.ts
CHANGED
|
@@ -551,7 +551,7 @@ export const SchemaNodeMethods = {
|
|
|
551
551
|
schema.$id = resolveUri(schema.$id || url);
|
|
552
552
|
}
|
|
553
553
|
|
|
554
|
-
const node = this as SchemaNode;
|
|
554
|
+
const node = this as SchemaNode & { schemaErrors?: JsonError[]; schemaAnnotations: JsonAnnotation[] };
|
|
555
555
|
const { context } = node;
|
|
556
556
|
const schemaId = isJsonSchema(schema) ? (node.context.draft ?? schema.$schema) : undefined;
|
|
557
557
|
const draft = getDraft(context.drafts, schemaId ?? context.rootNode.schema?.$schema);
|
|
@@ -576,7 +576,22 @@ export const SchemaNodeMethods = {
|
|
|
576
576
|
|
|
577
577
|
remoteNode.context.rootNode = remoteNode;
|
|
578
578
|
remoteNode.context.remotes[resolveUri(url)] = remoteNode;
|
|
579
|
-
|
|
579
|
+
|
|
580
|
+
// parse and validate schema
|
|
581
|
+
// @todo this is a duplicated to compileSchema
|
|
582
|
+
let schemaValidation = addKeywords(remoteNode).filter((err) => err != null);
|
|
583
|
+
schemaValidation = sanitizeErrors(schemaValidation);
|
|
584
|
+
const schemaErrors: JsonError[] = [];
|
|
585
|
+
const schemaAnnotations: JsonAnnotation[] = [];
|
|
586
|
+
schemaValidation.forEach((error) => {
|
|
587
|
+
if (isJsonError(error)) {
|
|
588
|
+
schemaErrors.push(error);
|
|
589
|
+
} else if (isJsonAnnotation(error)) {
|
|
590
|
+
schemaAnnotations.push(error);
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
node.schemaErrors = schemaErrors;
|
|
594
|
+
node.schemaAnnotations = schemaAnnotations;
|
|
580
595
|
|
|
581
596
|
return node;
|
|
582
597
|
},
|
|
@@ -631,6 +646,7 @@ export function addKeywords(node: SchemaNode) {
|
|
|
631
646
|
).forEach((keyword) => {
|
|
632
647
|
errors.push(
|
|
633
648
|
node.createAnnotation("unknown-keyword-warning", {
|
|
649
|
+
$id: node.$id,
|
|
634
650
|
pointer: `${node.schemaLocation}/${keyword}`,
|
|
635
651
|
schema: node.schema,
|
|
636
652
|
value: keyword,
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { compileSchema } from "./compileSchema";
|
|
2
2
|
import { strict as assert } from "assert";
|
|
3
|
-
import {
|
|
4
|
-
import { SchemaNode } from "./SchemaNode";
|
|
3
|
+
import { isSchemaNode, SchemaNode } from "./SchemaNode";
|
|
5
4
|
import { draft04 } from "./draft04";
|
|
6
5
|
import { draft07 } from "./draft07";
|
|
7
6
|
import { draft2020 } from "./draft2020";
|
|
@@ -106,6 +105,42 @@ describe("compileSchema remotes", () => {
|
|
|
106
105
|
const data = node.getData();
|
|
107
106
|
assert.deepEqual(data, { string: "a", number: 9 });
|
|
108
107
|
});
|
|
108
|
+
|
|
109
|
+
it("should resolve definition of remote schema", () => {
|
|
110
|
+
const node = compileSchema(
|
|
111
|
+
{
|
|
112
|
+
type: "object",
|
|
113
|
+
required: ["value"],
|
|
114
|
+
properties: {
|
|
115
|
+
value: { $ref: "https://remote.com/definitions.json#/$defs/property" }
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
drafts: [draft2020],
|
|
120
|
+
remotes: [
|
|
121
|
+
{
|
|
122
|
+
$id: "https://remote.com/definitions.json",
|
|
123
|
+
$defs: {
|
|
124
|
+
property: {
|
|
125
|
+
title: "remote boolean definition",
|
|
126
|
+
type: "boolean"
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
]
|
|
131
|
+
}
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const data = node.getData();
|
|
135
|
+
assert.deepEqual(data, { value: false });
|
|
136
|
+
|
|
137
|
+
const { node: valueNode } = node.getNode("#/value");
|
|
138
|
+
assert(isSchemaNode(valueNode), "expected returned node to be a valid SchemaNode");
|
|
139
|
+
assert.deepEqual(valueNode.schema, {
|
|
140
|
+
title: "remote boolean definition",
|
|
141
|
+
type: "boolean"
|
|
142
|
+
});
|
|
143
|
+
});
|
|
109
144
|
});
|
|
110
145
|
|
|
111
146
|
describe("compileSchema vocabulary", () => {
|
|
@@ -250,17 +285,3 @@ describe("compileSchema `schemaLocation`", () => {
|
|
|
250
285
|
assert.deepEqual(node?.schemaLocation, "#");
|
|
251
286
|
});
|
|
252
287
|
});
|
|
253
|
-
|
|
254
|
-
describe("compileSchema `errors`", () => {
|
|
255
|
-
it("draftEditor come with custom minLengthOneError", () => {
|
|
256
|
-
const { errors } = compileSchema(
|
|
257
|
-
{
|
|
258
|
-
type: "string",
|
|
259
|
-
minLength: 1
|
|
260
|
-
},
|
|
261
|
-
{ drafts: [draftEditor] }
|
|
262
|
-
).validate("");
|
|
263
|
-
assert.equal(errors.length, 1);
|
|
264
|
-
assert.deepEqual(errors[0].code, "min-length-one-error");
|
|
265
|
-
});
|
|
266
|
-
});
|
package/src/compileSchema.ts
CHANGED
|
@@ -66,7 +66,7 @@ export type CompileOptions = {
|
|
|
66
66
|
/**
|
|
67
67
|
* Set node and its remote schemata as remote schemata for this node and schema to resolve $ref
|
|
68
68
|
*/
|
|
69
|
-
remote?: SchemaNode;
|
|
69
|
+
remote?: SchemaNode & { schemaErrors?: JsonError[]; schemaAnnotations: JsonAnnotation[] };
|
|
70
70
|
/**
|
|
71
71
|
* a list of remotes to add, requires a unique $id for each schema. Will be ignored if `remote` is set
|
|
72
72
|
*/
|
|
@@ -180,11 +180,19 @@ export function compileSchema(schema: JsonSchema | BooleanSchema, options: Compi
|
|
|
180
180
|
const schemaAnnotations: JsonAnnotation[] = [];
|
|
181
181
|
schemaValidation.forEach((error) => {
|
|
182
182
|
if (isJsonError(error)) {
|
|
183
|
+
error.data.schemaId = node.context.rootNode.$id ?? "#";
|
|
183
184
|
schemaErrors.push(error);
|
|
184
185
|
} else if (isJsonAnnotation(error)) {
|
|
186
|
+
error.data.schemaId = node.context.rootNode.$id ?? "#";
|
|
185
187
|
schemaAnnotations.push(error);
|
|
186
188
|
}
|
|
187
189
|
});
|
|
190
|
+
if (Array.isArray(remote?.schemaErrors)) {
|
|
191
|
+
schemaErrors.push(...remote.schemaErrors);
|
|
192
|
+
}
|
|
193
|
+
if (Array.isArray(remote?.schemaAnnotations)) {
|
|
194
|
+
schemaAnnotations.push(...remote.schemaAnnotations);
|
|
195
|
+
}
|
|
188
196
|
|
|
189
197
|
if (options.throwOnInvalidSchema && schemaErrors.length > 0) {
|
|
190
198
|
const error = new Error("Invalid schema passed to compileSchema");
|
package/src/keywords/allOf.ts
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import { mergeSchema } from "../utils/mergeSchema";
|
|
2
|
-
import {
|
|
3
|
-
Keyword,
|
|
4
|
-
JsonSchemaReducerParams,
|
|
5
|
-
JsonSchemaValidatorParams,
|
|
6
|
-
ValidationReturnType,
|
|
7
|
-
ValidationAnnotation
|
|
8
|
-
} from "../Keyword";
|
|
2
|
+
import { Keyword, JsonSchemaReducerParams, JsonSchemaValidatorParams, ValidationReturnType } from "../Keyword";
|
|
9
3
|
import { SchemaNode } from "../types";
|
|
10
4
|
import { validateNode } from "../validateNode";
|
|
11
5
|
import { collectValidationErrors } from "src/utils/collectValidationErrors";
|
|
@@ -160,6 +160,17 @@ describe("keyword : oneof-fuzzy : reduce", () => {
|
|
|
160
160
|
assert.equal(res.oneOfIndex, 1, "should have exposed correct resolved oneOfIndex");
|
|
161
161
|
});
|
|
162
162
|
|
|
163
|
+
it("should resolve to best matching oneOf", () => {
|
|
164
|
+
const node = compileSchema({
|
|
165
|
+
oneOf: [
|
|
166
|
+
{ type: "array", items: { oneOf: [{ type: "string" }] } },
|
|
167
|
+
{ type: "array", items: { oneOf: [{ type: "number" }] } }
|
|
168
|
+
]
|
|
169
|
+
});
|
|
170
|
+
const res = reduceOneOfFuzzy({ node, data: [1, 2, "3"], pointer: "#", path: [] });
|
|
171
|
+
assert.deepEqual(res?.schema, { type: "array", items: { oneOf: [{ type: "number" }] } });
|
|
172
|
+
});
|
|
173
|
+
|
|
163
174
|
describe("object", () => {
|
|
164
175
|
it("should return schema with matching properties", () => {
|
|
165
176
|
const node = compileSchema({
|
package/src/keywords/oneOf.ts
CHANGED
|
@@ -257,6 +257,38 @@ export function reduceOneOfFuzzy({ node, data, pointer, path }: Omit<JsonSchemaR
|
|
|
257
257
|
});
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
+
const { node: reducedNode, error } = nodeOfItem.reduceNode(data, { pointer, path });
|
|
261
|
+
if (reducedNode) {
|
|
262
|
+
reducedNode.oneOfIndex = schemaOfIndex; // @evaluation-info
|
|
263
|
+
return reducedNode;
|
|
264
|
+
}
|
|
265
|
+
return error;
|
|
266
|
+
} else if (Array.isArray(data)) {
|
|
267
|
+
let nodeOfItem: SchemaNode | undefined;
|
|
268
|
+
let schemaOfIndex = -1;
|
|
269
|
+
let errorCount = Infinity;
|
|
270
|
+
|
|
271
|
+
for (let i = 0; i < node.oneOf.length; i += 1) {
|
|
272
|
+
const oneNode = node.oneOf[i];
|
|
273
|
+
const { errors } = oneNode.validate(data);
|
|
274
|
+
const nextErrorCount = errors.length;
|
|
275
|
+
|
|
276
|
+
if (nextErrorCount < errorCount) {
|
|
277
|
+
errorCount = nextErrorCount;
|
|
278
|
+
nodeOfItem = oneNode;
|
|
279
|
+
schemaOfIndex = i;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (nodeOfItem === undefined) {
|
|
284
|
+
return node.createError("one-of-error", {
|
|
285
|
+
value: JSON.stringify(data),
|
|
286
|
+
pointer,
|
|
287
|
+
schema: node.schema,
|
|
288
|
+
oneOf: node.schema.oneOf
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
260
292
|
const { node: reducedNode, error } = nodeOfItem.reduceNode(data, { pointer, path });
|
|
261
293
|
if (reducedNode) {
|
|
262
294
|
reducedNode.oneOfIndex = schemaOfIndex; // @evaluation-info
|
|
@@ -139,6 +139,27 @@ describe("keyword : propertyDependencies : validate", () => {
|
|
|
139
139
|
const { errors } = node.validate([{ propertyName: "propertyValue", type: "headline", test: 123 }]);
|
|
140
140
|
assert.equal(errors.length, 0);
|
|
141
141
|
});
|
|
142
|
+
|
|
143
|
+
it("should safely support __proto__ as property dependency key", () => {
|
|
144
|
+
const schema = JSON.parse(`{
|
|
145
|
+
"type": "array",
|
|
146
|
+
"items": {
|
|
147
|
+
"propertyDependencies": {
|
|
148
|
+
"__proto__": {
|
|
149
|
+
"polluted": {
|
|
150
|
+
"type": "object",
|
|
151
|
+
"required": ["safe"]
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}`);
|
|
157
|
+
const node = compileSchema(schema, { drafts });
|
|
158
|
+
const data = JSON.parse(`[{ "__proto__": "polluted" }]`);
|
|
159
|
+
|
|
160
|
+
const { errors } = node.validate(data);
|
|
161
|
+
assert.equal(errors.length, 1);
|
|
162
|
+
});
|
|
142
163
|
});
|
|
143
164
|
|
|
144
165
|
describe("keyword : propertyDependencies : validate", () => {
|
|
@@ -24,12 +24,13 @@ function findMatchingSchemata(node: SchemaNode, data: Record<string, unknown>) {
|
|
|
24
24
|
const matchingSchemata: { property: string; value: string; node: SchemaNode }[] = [];
|
|
25
25
|
for (const propertyName of dependentPropertyNames) {
|
|
26
26
|
if (hasProperty(data, propertyName)) {
|
|
27
|
-
const
|
|
28
|
-
|
|
27
|
+
const dependentValues = dependentProperties[propertyName];
|
|
28
|
+
const value = `${data[propertyName]}`;
|
|
29
|
+
if (hasProperty(dependentValues, value)) {
|
|
29
30
|
matchingSchemata.push({
|
|
30
31
|
property: propertyName,
|
|
31
|
-
value
|
|
32
|
-
node:
|
|
32
|
+
value,
|
|
33
|
+
node: dependentValues[value]
|
|
33
34
|
});
|
|
34
35
|
}
|
|
35
36
|
}
|
|
@@ -82,7 +83,7 @@ function parsePropertyDependencies(node: SchemaNode) {
|
|
|
82
83
|
message: `Keyword '${KEYWORD}' must be an object - received '${typeof propertyDependencies}'`
|
|
83
84
|
});
|
|
84
85
|
}
|
|
85
|
-
const parsed: Record<string, Record<string, SchemaNode>> =
|
|
86
|
+
const parsed: Record<string, Record<string, SchemaNode>> = Object.create(null);
|
|
86
87
|
const errors: ValidationAnnotation[] = [];
|
|
87
88
|
Object.keys(propertyDependencies).map((propertyName) => {
|
|
88
89
|
const values = propertyDependencies[propertyName];
|
|
@@ -110,7 +111,7 @@ function parsePropertyDependencies(node: SchemaNode) {
|
|
|
110
111
|
);
|
|
111
112
|
return;
|
|
112
113
|
}
|
|
113
|
-
parsed[propertyName] = parsed[propertyName] ??
|
|
114
|
+
parsed[propertyName] = parsed[propertyName] ?? Object.create(null);
|
|
114
115
|
parsed[propertyName][value] = node.compileSchema(
|
|
115
116
|
schema,
|
|
116
117
|
`${node.evaluationPath}/${KEYWORD}/${propertyName}/${value}`,
|
|
@@ -88,7 +88,30 @@ describe("getChildSelection", () => {
|
|
|
88
88
|
number: { type: "number" },
|
|
89
89
|
string: { type: "string" }
|
|
90
90
|
}
|
|
91
|
-
}).getChildSelection(
|
|
91
|
+
}).getChildSelection(1);
|
|
92
|
+
|
|
93
|
+
assert(!isJsonError(result));
|
|
94
|
+
assert.deepEqual(result.length, 2);
|
|
95
|
+
assert.deepEqual(
|
|
96
|
+
result.map((n) => n.schema),
|
|
97
|
+
[{ type: "string" }, { type: "number" }]
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("should resolve items $ref", () => {
|
|
102
|
+
const result = compileSchema({
|
|
103
|
+
type: "array",
|
|
104
|
+
items: {
|
|
105
|
+
$ref: "#/$defs/oneOfRef"
|
|
106
|
+
},
|
|
107
|
+
$defs: {
|
|
108
|
+
oneOfRef: {
|
|
109
|
+
oneOf: [{ $ref: "#/$defs/string" }, { $ref: "#/$defs/number" }]
|
|
110
|
+
},
|
|
111
|
+
number: { type: "number" },
|
|
112
|
+
string: { type: "string" }
|
|
113
|
+
}
|
|
114
|
+
}).getChildSelection(1);
|
|
92
115
|
|
|
93
116
|
assert(!isJsonError(result));
|
|
94
117
|
assert.deepEqual(result.length, 2);
|
|
@@ -6,12 +6,17 @@ import { isSchemaNode, JsonError, SchemaNode } from "../types";
|
|
|
6
6
|
* a list with a single item will be returned
|
|
7
7
|
*/
|
|
8
8
|
export function getChildSelection(node: SchemaNode, property: string | number) {
|
|
9
|
+
if (node.items) {
|
|
10
|
+
const items = node.items.resolveRef();
|
|
11
|
+
if (items?.oneOf) {
|
|
12
|
+
return items.oneOf.map((childNode: SchemaNode) => childNode.resolveRef());
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
9
16
|
if (node.oneOf) {
|
|
10
17
|
return node.oneOf.map((childNode: SchemaNode) => childNode.resolveRef());
|
|
11
18
|
}
|
|
12
|
-
|
|
13
|
-
return node.items.oneOf.map((childNode: SchemaNode) => childNode.resolveRef());
|
|
14
|
-
}
|
|
19
|
+
|
|
15
20
|
// array.items[] found
|
|
16
21
|
if (node.prefixItems && node.prefixItems.length > +property) {
|
|
17
22
|
const { node: childNode, error } = node.getNodeChild(property);
|
|
@@ -250,6 +250,18 @@ describe("validateSchema", () => {
|
|
|
250
250
|
assert.equal(schemaErrors[0].data.pointer, "#/uniqueItems");
|
|
251
251
|
});
|
|
252
252
|
|
|
253
|
+
describe("remotes", () => {
|
|
254
|
+
it("should return errors of remotes", () => {
|
|
255
|
+
const { schemaErrors } = compileSchema(
|
|
256
|
+
{},
|
|
257
|
+
{ remotes: [{ $id: "https://remote.com/error.json", anyOf: [999] }] }
|
|
258
|
+
);
|
|
259
|
+
assert.equal(schemaErrors?.length, 1);
|
|
260
|
+
assert.equal(schemaErrors[0].data.pointer, "#/anyOf/0");
|
|
261
|
+
assert.equal(schemaErrors[0].data.schemaId, "https://remote.com/error.json");
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
253
265
|
describe("annotations", () => {
|
|
254
266
|
it("should return unknown keywords as annotation", () => {
|
|
255
267
|
const { schemaAnnotations } = compileSchema({
|