json-schema-library 11.5.0 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "json-schema-library",
3
- "version": "11.5.0",
3
+ "version": "11.6.0",
4
4
  "description": "Customizable and hackable json-validator and json-schema utilities for traversal, data generation and validation",
5
5
  "types": "./dist/index.d.cts",
6
6
  "exports": {
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
- addKeywords(remoteNode);
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 { draftEditor } from "./draftEditor";
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
- });
@@ -66,7 +66,10 @@ 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
+ /**
71
+ * a list of remotes to add, requires a unique $id for each schema. Will be ignored if `remote` is set
72
+ */
70
73
  remotes?: JsonSchema[];
71
74
  /**
72
75
  * Enables `format`-keyword assertions when this is set tor `true` or sets assertion as defined by
@@ -177,11 +180,19 @@ export function compileSchema(schema: JsonSchema | BooleanSchema, options: Compi
177
180
  const schemaAnnotations: JsonAnnotation[] = [];
178
181
  schemaValidation.forEach((error) => {
179
182
  if (isJsonError(error)) {
183
+ error.data.schemaId = node.context.rootNode.$id ?? "#";
180
184
  schemaErrors.push(error);
181
185
  } else if (isJsonAnnotation(error)) {
186
+ error.data.schemaId = node.context.rootNode.$id ?? "#";
182
187
  schemaAnnotations.push(error);
183
188
  }
184
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
+ }
185
196
 
186
197
  if (options.throwOnInvalidSchema && schemaErrors.length > 0) {
187
198
  const error = new Error("Invalid schema passed to compileSchema");
@@ -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}`,
@@ -1,4 +1,5 @@
1
1
  import { compileSchema } from "../compileSchema";
2
+ import { draft2020 } from "../draft2020";
2
3
  import { strict as assert } from "assert";
3
4
 
4
5
  describe("keyword : unevaluatedProperties : validation", () => {
@@ -13,4 +14,60 @@ describe("keyword : unevaluatedProperties : validation", () => {
13
14
  });
14
15
  assert.equal(errors.length, 0);
15
16
  });
17
+
18
+ it("should not return unevaluated-property-error for a property that fails format validation", () => {
19
+ const node = compileSchema(
20
+ {
21
+ type: "object",
22
+ properties: {
23
+ name: { type: "string" },
24
+ email: { type: "string", format: "email" }
25
+ },
26
+ unevaluatedProperties: false
27
+ },
28
+ { drafts: [draft2020] }
29
+ );
30
+
31
+ const { errors } = node.validate({ name: "Alice", email: "not-an-email" });
32
+
33
+ const unevaluatedErrors = errors.filter((e) => e.code === "unevaluated-property-error");
34
+ assert.equal(unevaluatedErrors.length, 0, "should not flag email as unevaluated");
35
+ });
36
+
37
+ it("should not return unevaluated-property-error for a property that fails type validation", () => {
38
+ const node = compileSchema(
39
+ {
40
+ type: "object",
41
+ properties: {
42
+ name: { type: "string" },
43
+ age: { type: "number" }
44
+ },
45
+ unevaluatedProperties: false
46
+ },
47
+ { drafts: [draft2020] }
48
+ );
49
+
50
+ const { errors } = node.validate({ name: "Alice", age: "not-a-number" });
51
+
52
+ const unevaluatedErrors = errors.filter((e) => e.code === "unevaluated-property-error");
53
+ assert.equal(unevaluatedErrors.length, 0, "should not flag age as unevaluated");
54
+ });
55
+
56
+ it("should still return unevaluated-property-error for truly unknown properties", () => {
57
+ const node = compileSchema(
58
+ {
59
+ type: "object",
60
+ properties: {
61
+ name: { type: "string" }
62
+ },
63
+ unevaluatedProperties: false
64
+ },
65
+ { drafts: [draft2020] }
66
+ );
67
+
68
+ const { errors } = node.validate({ name: "Alice", unknown: "value" });
69
+
70
+ const unevaluatedErrors = errors.filter((e) => e.code === "unevaluated-property-error");
71
+ assert.equal(unevaluatedErrors.length, 1, "should flag unknown as unevaluated");
72
+ });
16
73
  });
@@ -56,6 +56,13 @@ function validateUnevaluatedProperties({ node, data, pointer, path }: JsonSchema
56
56
 
57
57
  const errors: ValidationReturnType = [];
58
58
  for (const propertyName of unevaluated) {
59
+ // Properties defined directly on this schema object are always
60
+ // evaluated by the "properties" keyword, regardless of whether the
61
+ // value passes validation (per JSON Schema spec, annotations from
62
+ // adjacent keywords are always collected)
63
+ if (node.properties?.[propertyName]) {
64
+ continue;
65
+ }
59
66
  if (isPropertyEvaluated({ node, data, key: propertyName, pointer, path })) {
60
67
  continue;
61
68
  }
@@ -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);
package/src/settings.ts CHANGED
@@ -19,7 +19,7 @@ export default {
19
19
  ],
20
20
  REGEX_FLAGS: "u",
21
21
  /** additional keywords that should not produce an unknown-keyword-warning */
22
- VALID_ANNOTATION_KEYWORDS: ["title", "description", "default"],
22
+ VALID_ANNOTATION_KEYWORDS: ["$id", "$schema", "title", "description", "default", "oneOfProperty"],
23
23
  /**
24
24
  * properties to keep from a $ref-schema when resolving a $ref (recursively)
25
25
  * this allows to overwrite specified properties locally on a $ref-definition
@@ -45,5 +45,5 @@ export default {
45
45
  * type: "object"
46
46
  * }
47
47
  */
48
- PROPERTIES_TO_MERGE: ["title", "description", "options", "x-options", "readOnly", "writeOnly"]
48
+ PROPERTIES_TO_MERGE: ["title", "description", "default", "options", "x-options", "readOnly", "writeOnly"]
49
49
  };
@@ -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({