json-schema-library 11.0.1 → 11.0.2

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.0.1",
3
+ "version": "11.0.2",
4
4
  "description": "Customizable and hackable json-validator and json-schema utilities for traversal, data generation and validation",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",
@@ -33,7 +33,7 @@
33
33
  "test:6:ci": "DISABLE_LOG=true mocha -R json 'src/tests/spec/draft06.spec.ts' > test-result-spec6.json; exit 0",
34
34
  "test:7": "mocha 'src/tests/spec/draft07.spec.ts'",
35
35
  "test:7:ci": "DISABLE_LOG=true mocha -R json 'src/tests/spec/draft07.spec.ts' > test-result-spec7.json; exit 0",
36
- "test:inspect": "yarn test:unit --inspect-brk",
36
+ "test:inspect": "NODE_OPTIONS='--inspect-brk' mocha 'src/**/*.test.ts'",
37
37
  "test:spec": "mocha 'src/tests/spec/*.spec.ts'",
38
38
  "test:unit": "mocha 'src/**/*.test.ts'",
39
39
  "test:unit:ci": "DISABLE_LOG=true mocha -R json 'src/**/*.test.ts' -R json > test-result-unit.json; exit 0"
package/src/SchemaNode.ts CHANGED
@@ -38,7 +38,7 @@ import { getNode } from "./getNode";
38
38
  import { getNodeChild } from "./getNodeChild";
39
39
  import { DataNode } from "./methods/toDataNodes";
40
40
 
41
- const { DYNAMIC_PROPERTIES, REGEX_FLAGS } = settings;
41
+ const { DYNAMIC_PROPERTIES, REGEX_FLAGS, DECLARATOR_ONEOF } = settings;
42
42
 
43
43
  export function isSchemaNode(value: unknown): value is SchemaNode {
44
44
  return isObject(value) && Array.isArray(value?.reducers) && Array.isArray(value?.resolvers);
@@ -194,12 +194,34 @@ interface SchemaNodeMethodsType {
194
194
  createAnnotation<T extends string = DefaultErrors>(code: T, data: AnnotationData, message?: string): JsonAnnotation;
195
195
  createSchema(data?: unknown): JsonSchema;
196
196
 
197
- // getNode overloads
197
+ /**
198
+ * Returns a node matching the given location (pointer) in data
199
+ *
200
+ * - the returned node will have a **reduced schema** based on given input data
201
+ * - return returned node $ref is resolved
202
+ *
203
+ * To resolve dynamic schema where the type of JSON Schema is evaluated by
204
+ * its value, a data object has to be passed in options.
205
+ *
206
+ * Per default this function will return `undefined` schema for valid properties
207
+ * that do not have a defined schema. Use the option `withSchemaWarning: true` to
208
+ * receive an error with `code: schema-warning` containing the location of its
209
+ * last evaluated json-schema.
210
+ *
211
+ * @returns { node } or { error } where node can also be undefined (valid but undefined)
212
+ */
198
213
  getNode(pointer: string, data: unknown, options: { withSchemaWarning: true } & GetNodeOptions): NodeOrError;
199
214
  getNode(pointer: string, data: unknown, options: { createSchema: true } & GetNodeOptions): NodeOrError;
200
215
  getNode(pointer: string, data?: unknown, options?: GetNodeOptions): OptionalNodeOrError;
201
216
 
202
- // getNodeChild overloads
217
+ /**
218
+ * Returns the child for the given property-name or array-index
219
+ *
220
+ * - the returned child node is **not reduced**
221
+ * - a child node $ref is resolved
222
+ *
223
+ * @returns { node } or { error } where node can also be undefined (valid but undefined)
224
+ */
203
225
  getNodeChild(
204
226
  key: string | number,
205
227
  data: unknown,
@@ -431,7 +453,7 @@ export const SchemaNodeMethods = {
431
453
  }
432
454
 
433
455
  // remove dynamic properties of node
434
- workingNode.schema = omit(workingNode.schema, ...DYNAMIC_PROPERTIES);
456
+ workingNode.schema = omit(workingNode.schema, DECLARATOR_ONEOF, ...DYNAMIC_PROPERTIES);
435
457
  // @ts-expect-error string accessing schema props
436
458
  DYNAMIC_PROPERTIES.forEach((prop) => (workingNode[prop] = undefined));
437
459
  return { node: workingNode, error: undefined };
@@ -1,14 +1,14 @@
1
1
  import { extendDraft } from "./Draft";
2
- import { draft2019 } from "./draft2019";
2
+ import { draft2020 } from "./draft2020";
3
3
  import { oneOfFuzzyKeyword } from "./keywords/oneOf";
4
4
  import { render } from "./errors/render";
5
5
 
6
6
  /**
7
- * @draft-editor https://json-schema.org/draft/2019-09/release-notes
7
+ * @draft-editor https://json-schema.org/draft/2020-12/release-notes
8
8
  *
9
- * Uses Draft 2019-09 and changes resolveOneOf to be fuzzy
9
+ * Uses Draft 2020-12 and changes resolveOneOf to be fuzzy
10
10
  */
11
- export const draftEditor = extendDraft(draft2019, {
11
+ export const draftEditor = extendDraft(draft2020, {
12
12
  $schemaRegEx: ".",
13
13
  keywords: [oneOfFuzzyKeyword],
14
14
  errors: {
@@ -45,12 +45,13 @@ export const errors = {
45
45
  "min-items-one-error": "At least one item is required in `{{pointer}}`",
46
46
  "min-length-error": "Value `{{pointer}}` should have a minimum length of `{{minLength}}`, but got `{{length}}`.",
47
47
  "min-length-one-error": "A value is required in `{{pointer}}`",
48
- "missing-one-of-declarator-error": "Missing oneOf declarator `{{declarator}}` in `{{pointer}}`",
48
+ "missing-one-of-declarator-error": "Missing oneOf declarator `{{declarator}}` in schema `{{schemaLocation}}`",
49
49
  "min-properties-error":
50
50
  "Too few properties in `{{pointer}}`, should be at least `{{minProperties}}`, but got `{{length}}`",
51
51
  "missing-array-item-error": "Array at '{{pointer}}' has a missing item at '{{key}}'",
52
52
  "missing-dependency-error": "The required propery '{{missingProperty}}' in `{{pointer}}` is missing",
53
- "missing-one-of-property-error": "Value at `{{pointer}}` property: `{{property}}`",
53
+ "missing-one-of-property-error":
54
+ "Value at `{{pointer}}` must be object or array and have a property ${oneOfProperty}: ${value}",
54
55
  "multiple-of-error": "Expected `{{value}}` in `{{pointer}}` to be multiple of `{{multipleOf}}`",
55
56
  "multiple-one-of-error": "Value `{{value}}` should not match multiple schemas in oneOf `{{matches}}`",
56
57
  "no-additional-properties-error": "Additional property `{{property}}` in `{{pointer}}` is not allowed",
@@ -0,0 +1,43 @@
1
+ import { compileSchema } from "./compileSchema";
2
+ import { strict as assert } from "assert";
3
+ import { isSchemaNode } from "./types";
4
+ import settings from "./settings";
5
+ const DECLARATOR_ONEOF = settings.DECLARATOR_ONEOF;
6
+
7
+ describe("getNodeChild.oneOfProperty", () => {
8
+ describe("", () => {
9
+ it("should return resolved reference (reduced oneOfNode)", () => {
10
+ const node = compileSchema({
11
+ items: {
12
+ [DECLARATOR_ONEOF]: "name",
13
+ oneOf: [{ $ref: "#/$defs/first" }, { $ref: "#/$defs/second" }]
14
+ },
15
+ $defs: {
16
+ first: {
17
+ properties: { name: { type: "string", const: "first" }, title: { type: "number" } }
18
+ },
19
+ second: {
20
+ properties: { name: { type: "string", const: "second" }, title: { type: "number" } }
21
+ }
22
+ }
23
+ });
24
+
25
+ // precondition
26
+ const reducedItems = node.items?.reduceNode({ name: "second" });
27
+ assert(reducedItems && reducedItems.node, "should have successfully resolved items schema directly");
28
+ assert.deepEqual(
29
+ reducedItems.node?.schema,
30
+ {
31
+ properties: { name: { type: "string", const: "second" }, title: { type: "number" } }
32
+ },
33
+ "should have correctly resolved items-schema directly"
34
+ );
35
+
36
+ const { node: res } = node.getNode("#/0", [{ name: "second" }]);
37
+ assert(isSchemaNode(res), "expected result to a node");
38
+ assert.deepEqual(res.schema, {
39
+ properties: { name: { type: "string", const: "second" }, title: { type: "number" } }
40
+ });
41
+ });
42
+ });
43
+ });
@@ -520,7 +520,7 @@ describe("compileSchema : getNode", () => {
520
520
  assert.deepEqual(node.schema, { type: "array", minItems: 2 });
521
521
  });
522
522
 
523
- it("should mrege title from local schema", () => {
523
+ it("should merge title from local schema", () => {
524
524
  const { node } = compileSchema({
525
525
  type: "array",
526
526
  prefixItems: [{ title: "from ref", $ref: "/$defs/target" }],
package/src/getNode.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { GetNodeOptions, SchemaNode } from "./SchemaNode";
1
+ import { GetNodeOptions, isSchemaNode, SchemaNode } from "./SchemaNode";
2
2
  import { isJsonError, NodeOrError, OptionalNodeOrError } from "./types";
3
3
  import { split } from "@sagold/json-pointer";
4
4
  import { getValue } from "./utils/getValue";
@@ -7,9 +7,13 @@ import { getValue } from "./utils/getValue";
7
7
  export function getNode(pointer: string, data: unknown, options: { withSchemaWarning: true } & GetNodeOptions): NodeOrError;
8
8
  export function getNode(pointer: string, data: unknown, options: { createSchema: true } & GetNodeOptions): NodeOrError;
9
9
  export function getNode(pointer: string, data?: unknown, options?: GetNodeOptions): OptionalNodeOrError;
10
+
10
11
  /**
11
12
  * Returns a node containing JSON Schema of a data JSON Pointer.
12
13
  *
14
+ * - the returned node will have a reduced schema based on given input data
15
+ * - the returned node $ref is resolved
16
+ *
13
17
  * To resolve dynamic schema where the type of JSON Schema is evaluated by
14
18
  * its value, a data object has to be passed in options.
15
19
  *
@@ -49,6 +53,15 @@ export function getNode(
49
53
  currentNode = result.node;
50
54
  data = getValue(data, keys[i]);
51
55
  }
52
- const result = currentNode.resolveRef(options);
53
- return isJsonError(result) ? { node: undefined, error: result } : { node: result, error: undefined };
56
+
57
+ const { node: reducedNode, error: reduceError } = currentNode.resolveRef(options).reduceNode(data);
58
+
59
+ if (isJsonError(reduceError)) {
60
+ return { node: undefined, error: reduceError };
61
+ }
62
+ if (isSchemaNode(reducedNode)) {
63
+ return { node: reducedNode, error: undefined };
64
+ }
65
+
66
+ return { error: undefined };
54
67
  }
@@ -2,67 +2,73 @@ import { GetNodeOptions, isSchemaNode, SchemaNode } from "./SchemaNode";
2
2
  import { isJsonError, NodeOrError, OptionalNodeOrError } from "./types";
3
3
  import { getValue } from "./utils/getValue";
4
4
 
5
- // prettier-ignore
6
- export function getNodeChild(key: string | number, data: unknown, options: { withSchemaWarning: true } & GetNodeOptions): NodeOrError;
7
- // prettier-ignore
8
- export function getNodeChild(key: string | number, data: unknown, options: { createSchema: true } & GetNodeOptions): NodeOrError;
5
+ export function getNodeChild(key: string | number, data: unknown, options: { withSchemaWarning: true } & GetNodeOptions): NodeOrError; // prettier-ignore
6
+ export function getNodeChild(key: string | number, data: unknown, options: { createSchema: true } & GetNodeOptions): NodeOrError; // prettier-ignore
9
7
  export function getNodeChild(key: string | number, data?: unknown, options?: GetNodeOptions): OptionalNodeOrError;
10
8
 
11
9
  /**
12
- * @returns child node identified by property as SchemaNode
10
+ * Returns the child for the given property-name or array-index
11
+ *
12
+ * - the returned child node is **not reduced**
13
+ * - a child node $ref is resolved
14
+ *
15
+ * @returns { node } or { error } where node can also be undefined (valid but undefined)
13
16
  */
14
17
  export function getNodeChild(
15
18
  key: string | number,
16
19
  data?: unknown,
17
20
  options: GetNodeOptions = {}
18
- ): OptionalNodeOrError | NodeOrError | object {
21
+ ): OptionalNodeOrError | NodeOrError {
19
22
  options.path = options.path ?? [];
20
-
21
23
  options.withSchemaWarning = options.withSchemaWarning ?? false;
22
24
  options.pointer = options.pointer ?? "#";
23
25
  const { path, pointer } = options;
24
26
 
27
+ // reduce parent
25
28
  // @ts-expect-error implicitely any
26
- let node = this as SchemaNode;
27
- if (node.reducers.length) {
28
- const result = node.reduceNode(data, { key, path, pointer });
29
+ let parentNode = this as SchemaNode;
30
+ if (parentNode.reducers.length) {
31
+ const result = parentNode.reduceNode(data, { key, path, pointer });
29
32
  if (result.error) {
30
33
  return result;
31
34
  }
32
35
  if (isSchemaNode(result.node)) {
33
- node = result.node;
36
+ parentNode = result.node;
34
37
  }
35
38
  }
36
39
 
37
- for (const resolver of node.resolvers) {
38
- const schemaNode = resolver({ data, key, node });
39
- if (isSchemaNode(schemaNode)) {
40
- return { node: schemaNode.resolveRef({ pointer, path }), error: undefined };
41
- }
40
+ // find child node
41
+ for (const resolver of parentNode.resolvers) {
42
+ const schemaNode = resolver({ data, key, node: parentNode });
43
+ // a matching resolver found an error, return
42
44
  if (isJsonError(schemaNode)) {
43
45
  return { node: undefined, error: schemaNode };
44
46
  }
47
+ // a matching resolver found a child node, return
48
+ if (isSchemaNode(schemaNode)) {
49
+ return { node: schemaNode.resolveRef({ pointer, path }), error: undefined };
50
+ }
45
51
  }
46
52
 
47
- const referencedNode = node.resolveRef({ path });
48
- if (referencedNode !== node) {
49
- return referencedNode.getNodeChild(key, data, options);
50
- }
51
-
53
+ // no child node was found, but the child node is valid
52
54
  if (options.createSchema === true) {
53
- const newNode = node.compileSchema(
54
- node.createSchema(getValue(data, key)),
55
- `${node.evaluationPath}/additional`,
56
- `${node.schemaLocation}/additional`
55
+ const newNode = parentNode.compileSchema(
56
+ parentNode.createSchema(getValue(data, key)),
57
+ `${parentNode.evaluationPath}/additional`,
58
+ `${parentNode.schemaLocation}/additional`
57
59
  );
58
60
  return { node: newNode, error: undefined };
59
61
  }
60
62
 
61
63
  if (options.withSchemaWarning === true) {
62
- const error = node.createError("schema-warning", { pointer, value: data, schema: node.schema, key });
64
+ const error = parentNode.createError("schema-warning", {
65
+ pointer,
66
+ value: data,
67
+ schema: parentNode.schema,
68
+ key
69
+ });
63
70
  return { node: undefined, error };
64
71
  }
65
72
 
66
- // throw new Error("getNodeChild failed retrieving node or error");
67
- return {};
73
+ return { node: undefined };
68
74
  }
@@ -1,6 +1,6 @@
1
1
  import { strict as assert } from "assert";
2
2
  import { compileSchema } from "../compileSchema";
3
- import { isJsonError } from "../types";
3
+ import { isJsonError, isSchemaNode } from "../types";
4
4
  import { reduceOneOfDeclarator, reduceOneOfFuzzy } from "./oneOf";
5
5
  import settings from "../settings";
6
6
  import { draftEditor } from "../draftEditor";
@@ -350,33 +350,56 @@ describe("keyword : oneof-property : reduce", () => {
350
350
  assert(isJsonError(res), "expected result to be an error");
351
351
  assert.deepEqual(res.code, "missing-one-of-property-error");
352
352
  });
353
- });
354
353
 
355
- describe("array", () => {
356
- it("should return an error if no oneOfProperty could be matched", () => {
354
+ it("should return correct reference", () => {
357
355
  const node = compileSchema({
358
- oneOf: [
359
- {
360
- type: "object",
361
- required: ["title"],
362
- properties: {
363
- title: {
364
- type: "string"
365
- }
366
- }
356
+ [DECLARATOR_ONEOF]: "name",
357
+ oneOf: [{ $ref: "#/$defs/first" }, { $ref: "#/$defs/second" }],
358
+ $defs: {
359
+ first: {
360
+ properties: { name: { type: "string", const: "first" }, title: { type: "number" } }
361
+ },
362
+ second: {
363
+ properties: { name: { type: "string", const: "second" }, title: { type: "number" } }
367
364
  }
368
- ]
365
+ }
366
+ });
367
+ const res = reduceOneOfDeclarator({ node, data: { name: "second" }, pointer: "#", path: [] });
368
+ assert(isSchemaNode(res), "expected result to be a node");
369
+ assert.deepEqual(res.schema, {
370
+ properties: { name: { type: "string", const: "second" }, title: { type: "number" } }
371
+ });
372
+ });
373
+
374
+ it("should return correct reference even if data is not fully valid", () => {
375
+ const node = compileSchema({
376
+ [DECLARATOR_ONEOF]: "name",
377
+ oneOf: [{ $ref: "#/$defs/first" }, { $ref: "#/$defs/second" }],
378
+ $defs: {
379
+ first: {
380
+ properties: { name: { type: "string", const: "first" }, title: { type: "number" } }
381
+ },
382
+ second: {
383
+ properties: { name: { type: "string", const: "second" }, title: { type: "number" } }
384
+ }
385
+ }
369
386
  });
370
387
  const res = reduceOneOfDeclarator({
371
388
  node,
372
- data: { name: "2", title: "not a number" },
389
+ data: { name: "first", title: "not a number" },
373
390
  pointer: "#",
374
391
  path: []
375
392
  });
376
- assert(isJsonError(res), "expected result to be an error");
377
- assert.deepEqual(res.code, "missing-one-of-property-error");
393
+ assert(isSchemaNode(res), "expected result to be a node");
394
+ assert.deepEqual(res.schema, {
395
+ properties: { name: { type: "string", const: "first" }, title: { type: "number" } }
396
+ });
378
397
  });
379
398
  });
399
+
400
+ describe("array", () => {
401
+ // TODO test access on array-item as oneOfProperty id (oneOfProperty: "0")
402
+ });
380
403
  });
381
404
 
382
405
  describe("keyword : oneof-fuzzy : validate", () => {
@@ -101,42 +101,57 @@ function reduceOneOf({ node, data, pointer, path }: Omit<JsonSchemaReducerParams
101
101
  });
102
102
  }
103
103
 
104
+ /**
105
+ * Returns matching oneOf schema identified by matching schema for oneOfProperty
106
+ */
104
107
  export function reduceOneOfDeclarator({ node, data, pointer, path }: Omit<JsonSchemaReducerParams, "key">) {
105
108
  if (node.oneOf == null) {
106
109
  return;
107
110
  }
108
111
 
109
- const errors: ValidationReturnType = [];
110
112
  const oneOfProperty = node.schema[DECLARATOR_ONEOF];
111
- const oneOfValue = getValue(data, oneOfProperty);
113
+ const oneOfPropertyValue = getValue(data, oneOfProperty);
112
114
 
113
- if (oneOfValue === undefined) {
115
+ // in this case, we also fail when data undefined as this always is valid,
116
+ // but not in context on an expected oneOfProperty
117
+ if (data === undefined || oneOfPropertyValue === undefined) {
114
118
  return node.createError("missing-one-of-property-error", {
115
- property: oneOfProperty,
119
+ oneOfProperty,
116
120
  pointer,
117
121
  schema: node.schema,
118
122
  value: data
119
123
  });
120
124
  }
121
125
 
126
+ // find oneOf schema that has a matching oneOfProperty to the current input data
127
+ // TODO throw an error if multiple matches were found
128
+ const errors: ValidationReturnType = [];
122
129
  for (let i = 0; i < node.oneOf.length; i += 1) {
123
130
  const { node: resultNode } = node.oneOf[i].getNodeChild(oneOfProperty, data);
124
131
  if (!isSchemaNode(resultNode)) {
132
+ // one of the oneOf schemas has a missing oneOfTypeProperty
133
+ // TODO this still might succeed
134
+ // TODO there is a possibility this throws an invalid error as we use input data
125
135
  return node.createError("missing-one-of-declarator-error", {
126
136
  declarator: DECLARATOR_ONEOF,
127
137
  oneOfProperty,
128
- schemaPointer: node.oneOf[i].schemaLocation,
138
+ schemaLocation: node.oneOf[i].schemaLocation,
129
139
  pointer: `${pointer}/oneOf/${i}`,
130
140
  schema: node.schema,
131
141
  value: data
132
142
  });
133
143
  }
134
144
 
135
- const result = sanitizeErrors(validateNode(resultNode, oneOfValue, pointer, path));
136
- // result = result.filter(errorOrPromise);
145
+ // collect errors in case we fail finding a matching schema
146
+ const result = sanitizeErrors(
147
+ validateNode(resultNode, oneOfPropertyValue, `${pointer}/${oneOfProperty}`, path)
148
+ );
149
+
137
150
  if (result.length > 0) {
138
151
  errors.push(...result);
139
152
  } else {
153
+ // return at once when we found a schema
154
+ // TODO should check all oneOf-schema
140
155
  const { node: reducedNode } = node.oneOf[i].reduceNode(data, { pointer, path });
141
156
  if (reducedNode) {
142
157
  reducedNode.oneOfIndex = i; // @evaluation-info
@@ -147,7 +162,7 @@ export function reduceOneOfDeclarator({ node, data, pointer, path }: Omit<JsonSc
147
162
 
148
163
  return node.createError("one-of-property-error", {
149
164
  property: oneOfProperty,
150
- value: oneOfValue,
165
+ value: data,
151
166
  pointer,
152
167
  schema: node.schema,
153
168
  errors
@@ -34,6 +34,24 @@ describe("getChildSelection", () => {
34
34
  );
35
35
  });
36
36
 
37
+ it("should return resolved $ref", () => {
38
+ const result = compileSchema({
39
+ type: "array",
40
+ prefixItems: [{ $ref: "#/$defs/first" }, { $ref: "#/$defs/second" }],
41
+ $defs: {
42
+ first: { type: "string" },
43
+ second: { type: "number" }
44
+ }
45
+ }).getChildSelection(1);
46
+
47
+ assert(!isJsonError(result));
48
+ assert.deepEqual(result.length, 1);
49
+ assert.deepEqual(
50
+ result.map((n) => n.schema),
51
+ [{ type: "number" }]
52
+ );
53
+ });
54
+
37
55
  it("should return an empty array if items schema is undefined", () => {
38
56
  const result = compileSchema({
39
57
  type: "array",
@@ -5,7 +5,7 @@ import { isSchemaNode, JsonError, SchemaNode } from "../types";
5
5
  * could be added at the given property (e.g. item-index), thus an array of options is returned. In all other cases
6
6
  * a list with a single item will be returned
7
7
  */
8
- export function getChildSelection(node: SchemaNode, property: string | number): SchemaNode[] | JsonError {
8
+ export function getChildSelection(node: SchemaNode, property: string | number) {
9
9
  if (node.oneOf) {
10
10
  return node.oneOf.map((childNode: SchemaNode) => childNode.resolveRef());
11
11
  }
@@ -302,7 +302,6 @@ describe("compileSchema : reduceNode", () => {
302
302
  }).reduceNode({ id: "second" });
303
303
  assert.deepEqual(node.schema, {
304
304
  type: "object",
305
- oneOfProperty: "id",
306
305
  properties: { id: { const: "second" }, one: { type: "number" } }
307
306
  });
308
307
  });