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.
@@ -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 != null || isRequired || opts.addOptionalProps) {
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) => d[key] == null && (d[key] = getValue(data, 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
 
@@ -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({
@@ -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 value = data[propertyName];
28
- if (dependentProperties[propertyName][value as string]) {
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: `${value}`,
32
- node: dependentProperties[propertyName][value as string]
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("b");
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
- if (node.items?.oneOf) {
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",
@@ -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 != null || isRequired || opts.addOptionalProps) {
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) => d[key] == null && (d[key] = getValue(data, 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
 
@@ -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({