json-schema-library 11.5.1 → 11.6.1
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 +2 -2
- 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/draft2019-09/methods/getData.test.ts +1870 -1790
- package/src/draft2019-09/methods/getData.ts +34 -2
- 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/methods/getData.test.ts +73 -0
- package/src/methods/getData.ts +34 -3
- package/src/utils/getValue.ts +2 -1
- package/src/validateSchema.test.ts +12 -0
|
@@ -191,8 +191,19 @@ const TYPE: Record<string, (node: SchemaNode, data: unknown, opts: TemplateOptio
|
|
|
191
191
|
const input = getValue(data, propertyName);
|
|
192
192
|
const value = data === undefined || input === undefined ? getValue(template, propertyName) : input;
|
|
193
193
|
// Omit adding a property if it is not required or optional props should be added
|
|
194
|
-
if (value
|
|
194
|
+
if (value !== undefined || isRequired || opts.addOptionalProps) {
|
|
195
195
|
const propertyValue = propertyNode.getData(value, opts);
|
|
196
|
+
const dataType = getTypeOf(propertyValue);
|
|
197
|
+
if (
|
|
198
|
+
opts.removeInvalidData &&
|
|
199
|
+
// @attention we do not remove values that might have valid nested values
|
|
200
|
+
dataType !== "object" &&
|
|
201
|
+
dataType !== "array" &&
|
|
202
|
+
!propertyNode.validate(propertyValue).valid
|
|
203
|
+
) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
196
207
|
if (propertyValue !== undefined || opts.useTypeDefaults !== false) {
|
|
197
208
|
d[propertyName] = propertyValue;
|
|
198
209
|
}
|
|
@@ -250,7 +261,28 @@ const TYPE: Record<string, (node: SchemaNode, data: unknown, opts: TemplateOptio
|
|
|
250
261
|
}
|
|
251
262
|
} else {
|
|
252
263
|
// merge any missing data (additionals) to resulting object
|
|
253
|
-
Object.keys(data).forEach((key) =>
|
|
264
|
+
Object.keys(data).forEach((key) => {
|
|
265
|
+
// if we allow additionalProperties, but the current value has a schema that invalidates
|
|
266
|
+
// do not add this value
|
|
267
|
+
const dataType = getTypeOf(key);
|
|
268
|
+
if (
|
|
269
|
+
opts.removeInvalidData &&
|
|
270
|
+
node.properties &&
|
|
271
|
+
node.properties[key] &&
|
|
272
|
+
dataType !== "object" &&
|
|
273
|
+
dataType !== "array"
|
|
274
|
+
) {
|
|
275
|
+
const propertyNode = node.properties[key];
|
|
276
|
+
const propertyValue = propertyNode.getData(getValue(data, key), opts);
|
|
277
|
+
if (!propertyNode.validate(propertyValue).valid) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (d[key] == null) {
|
|
283
|
+
d[key] = getValue(data, key);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
254
286
|
}
|
|
255
287
|
}
|
|
256
288
|
|
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);
|
|
@@ -1450,6 +1450,79 @@ describe("getData", () => {
|
|
|
1450
1450
|
assert.deepEqual(res, {});
|
|
1451
1451
|
});
|
|
1452
1452
|
|
|
1453
|
+
it("should not remove 'null' values if specified in enum", () => {
|
|
1454
|
+
const node = compileSchema({
|
|
1455
|
+
type: "object",
|
|
1456
|
+
additionalProperties: false,
|
|
1457
|
+
properties: {
|
|
1458
|
+
keepA: {
|
|
1459
|
+
type: ["string", "null"],
|
|
1460
|
+
enum: ["Chicago", "Rome", null]
|
|
1461
|
+
},
|
|
1462
|
+
keepB: {
|
|
1463
|
+
type: ["null", "string"],
|
|
1464
|
+
enum: ["Chicago", null, "Rome"]
|
|
1465
|
+
},
|
|
1466
|
+
removeA: {
|
|
1467
|
+
type: ["string"],
|
|
1468
|
+
enum: ["Chicago", "Rome", null]
|
|
1469
|
+
},
|
|
1470
|
+
removeB: {
|
|
1471
|
+
type: ["string", "null"],
|
|
1472
|
+
enum: ["Chicago", "Rome"]
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
});
|
|
1476
|
+
|
|
1477
|
+
const res = node.getData(
|
|
1478
|
+
{ keepA: null, keepB: null, removeA: null, removeB: null },
|
|
1479
|
+
{ removeInvalidData: true }
|
|
1480
|
+
);
|
|
1481
|
+
|
|
1482
|
+
assert.deepEqual(res, { keepA: null, keepB: null });
|
|
1483
|
+
});
|
|
1484
|
+
|
|
1485
|
+
it("should not remove full object when a nested value fails validation", () => {
|
|
1486
|
+
const node = compileSchema({
|
|
1487
|
+
type: "object",
|
|
1488
|
+
additionalProperties: false,
|
|
1489
|
+
properties: {
|
|
1490
|
+
keep: {
|
|
1491
|
+
type: "object",
|
|
1492
|
+
additionalProperties: false,
|
|
1493
|
+
properties: {
|
|
1494
|
+
keep: { type: "number" },
|
|
1495
|
+
remove: { type: "number" }
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
});
|
|
1500
|
+
|
|
1501
|
+
const res = node.getData({ keep: { keep: 9, remove: "a" } }, { removeInvalidData: true });
|
|
1502
|
+
|
|
1503
|
+
assert.deepEqual(res, { keep: { keep: 9 } });
|
|
1504
|
+
});
|
|
1505
|
+
|
|
1506
|
+
it("should remove nested properties that fail validation", () => {
|
|
1507
|
+
const node = compileSchema({
|
|
1508
|
+
type: "object",
|
|
1509
|
+
additionalProperties: false,
|
|
1510
|
+
properties: {
|
|
1511
|
+
keep: {
|
|
1512
|
+
type: "object",
|
|
1513
|
+
properties: {
|
|
1514
|
+
keep: { type: "number" },
|
|
1515
|
+
remove: { type: "number" }
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
});
|
|
1520
|
+
|
|
1521
|
+
const res = node.getData({ keep: { keep: 9, remove: "a" } }, { removeInvalidData: true });
|
|
1522
|
+
|
|
1523
|
+
assert.deepEqual(res, { keep: { keep: 9 } });
|
|
1524
|
+
});
|
|
1525
|
+
|
|
1453
1526
|
it("should not add optional properties", () => {
|
|
1454
1527
|
const schema = {
|
|
1455
1528
|
type: "object",
|
package/src/methods/getData.ts
CHANGED
|
@@ -190,8 +190,19 @@ const TYPE: Record<string, (node: SchemaNode, data: unknown, opts: TemplateOptio
|
|
|
190
190
|
const input = getValue(data, propertyName);
|
|
191
191
|
const value = data === undefined || input === undefined ? getValue(template, propertyName) : input;
|
|
192
192
|
// Omit adding a property if it is not required or optional props should be added
|
|
193
|
-
if (value
|
|
193
|
+
if (value !== undefined || isRequired || opts.addOptionalProps) {
|
|
194
194
|
const propertyValue = propertyNode.getData(value, opts);
|
|
195
|
+
const dataType = getTypeOf(propertyValue);
|
|
196
|
+
if (
|
|
197
|
+
opts.removeInvalidData &&
|
|
198
|
+
// @attention we do not remove values that might have valid nested values
|
|
199
|
+
dataType !== "object" &&
|
|
200
|
+
dataType !== "array" &&
|
|
201
|
+
!propertyNode.validate(propertyValue).valid
|
|
202
|
+
) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
195
206
|
if (propertyValue !== undefined || opts.useTypeDefaults !== false) {
|
|
196
207
|
d[propertyName] = propertyValue;
|
|
197
208
|
}
|
|
@@ -229,7 +240,6 @@ const TYPE: Record<string, (node: SchemaNode, data: unknown, opts: TemplateOptio
|
|
|
229
240
|
});
|
|
230
241
|
}
|
|
231
242
|
|
|
232
|
-
// console.log("getData object", data, opts);
|
|
233
243
|
if (data) {
|
|
234
244
|
if (
|
|
235
245
|
opts.removeInvalidData === true &&
|
|
@@ -248,7 +258,28 @@ const TYPE: Record<string, (node: SchemaNode, data: unknown, opts: TemplateOptio
|
|
|
248
258
|
}
|
|
249
259
|
} else {
|
|
250
260
|
// merge any missing data (additionals) to resulting object
|
|
251
|
-
Object.keys(data).forEach((key) =>
|
|
261
|
+
Object.keys(data).forEach((key) => {
|
|
262
|
+
// if we allow additionalProperties, but the current value has a schema that invalidates
|
|
263
|
+
// do not add this value
|
|
264
|
+
const dataType = getTypeOf(key);
|
|
265
|
+
if (
|
|
266
|
+
opts.removeInvalidData &&
|
|
267
|
+
node.properties &&
|
|
268
|
+
node.properties[key] &&
|
|
269
|
+
dataType !== "object" &&
|
|
270
|
+
dataType !== "array"
|
|
271
|
+
) {
|
|
272
|
+
const propertyNode = node.properties[key];
|
|
273
|
+
const propertyValue = propertyNode.getData(getValue(data, key), opts);
|
|
274
|
+
if (!propertyNode.validate(propertyValue).valid) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (d[key] == null) {
|
|
280
|
+
d[key] = getValue(data, key);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
252
283
|
}
|
|
253
284
|
}
|
|
254
285
|
|
package/src/utils/getValue.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { isObject } from "../utils/isObject";
|
|
2
|
+
import { hasProperty } from "./hasProperty";
|
|
2
3
|
|
|
3
4
|
export function getValue(data: unknown, key: string | number) {
|
|
4
|
-
if (isObject(data)) {
|
|
5
|
+
if (isObject(data) && hasProperty(data, `${key}`)) {
|
|
5
6
|
return data[key];
|
|
6
7
|
} else if (Array.isArray(data)) {
|
|
7
8
|
return data[key as number];
|
|
@@ -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({
|