json-schema-library 11.0.4 → 11.1.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.
Files changed (58) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +112 -0
  3. package/dist/index.cjs +1 -1
  4. package/dist/index.d.cts +93 -16
  5. package/dist/index.d.mts +93 -16
  6. package/dist/index.mjs +1 -1
  7. package/dist/jlib.js +3 -3
  8. package/index.ts +10 -1
  9. package/package.json +11 -8
  10. package/src/Draft.ts +1 -1
  11. package/src/Keyword.ts +36 -10
  12. package/src/SchemaNode.ts +75 -16
  13. package/src/compileSchema.ts +53 -4
  14. package/src/errors/errors.ts +4 -1
  15. package/src/keywords/$defs.ts +34 -8
  16. package/src/keywords/$ref.ts +12 -0
  17. package/src/keywords/additionalProperties.ts +19 -8
  18. package/src/keywords/allOf.ts +44 -18
  19. package/src/keywords/anyOf.ts +38 -19
  20. package/src/keywords/contains.ts +21 -9
  21. package/src/keywords/dependencies.ts +37 -17
  22. package/src/keywords/dependentRequired.ts +56 -38
  23. package/src/keywords/dependentSchemas.ts +37 -13
  24. package/src/keywords/deprecated.ts +32 -8
  25. package/src/keywords/enum.ts +30 -8
  26. package/src/keywords/exclusiveMaximum.ts +21 -2
  27. package/src/keywords/exclusiveMinimum.ts +22 -3
  28. package/src/keywords/format.ts +21 -2
  29. package/src/keywords/ifthenelse.ts +49 -5
  30. package/src/keywords/items.ts +27 -13
  31. package/src/keywords/maxItems.ts +22 -2
  32. package/src/keywords/maxLength.ts +30 -9
  33. package/src/keywords/maxProperties.ts +30 -9
  34. package/src/keywords/maximum.ts +28 -8
  35. package/src/keywords/minItems.ts +30 -9
  36. package/src/keywords/minLength.ts +30 -9
  37. package/src/keywords/minProperties.ts +26 -5
  38. package/src/keywords/minimum.ts +32 -13
  39. package/src/keywords/multipleOf.ts +33 -12
  40. package/src/keywords/not.ts +23 -10
  41. package/src/keywords/oneOf.ts +29 -9
  42. package/src/keywords/pattern.ts +35 -9
  43. package/src/keywords/properties.ts +35 -12
  44. package/src/keywords/propertyDependencies.test.ts +180 -0
  45. package/src/keywords/propertyDependencies.ts +173 -0
  46. package/src/keywords/propertyNames.ts +26 -14
  47. package/src/keywords/required.ts +31 -8
  48. package/src/keywords/type.ts +53 -16
  49. package/src/keywords/unevaluatedItems.ts +24 -8
  50. package/src/keywords/unevaluatedProperties.ts +24 -7
  51. package/src/keywords/uniqueItems.ts +23 -4
  52. package/src/mergeNode.ts +9 -4
  53. package/src/settings.ts +2 -1
  54. package/src/types.ts +1 -1
  55. package/src/utils/isListOfStrings.ts +3 -0
  56. package/src/validate.test.ts +0 -2
  57. package/src/validateNode.ts +2 -2
  58. package/src/validateSchema.test.ts +312 -0
@@ -1,25 +1,48 @@
1
1
  import { isObject } from "../utils/isObject";
2
2
  import { Keyword, JsonSchemaValidatorParams } from "../Keyword";
3
3
  import { hasProperty } from "../utils/hasProperty";
4
+ import { SchemaNode } from "../SchemaNode";
5
+ import { isListOfStrings } from "../utils/isListOfStrings";
4
6
 
5
- export const requiredKeyword: Keyword = {
6
- id: "required",
7
- keyword: "required",
8
- addValidate: ({ schema }) => Array.isArray(schema.required),
7
+ const KEYWORD = "required";
8
+
9
+ export const requiredKeyword: Keyword<"required"> = {
10
+ id: KEYWORD,
11
+ keyword: KEYWORD,
12
+ parse: parseRequired,
13
+ addValidate: (node) => node.required != null,
9
14
  validate: validateRequired
10
15
  };
11
16
 
12
- function validateRequired({ node, data, pointer = "#" }: JsonSchemaValidatorParams) {
13
- const { schema } = node;
17
+ function parseRequired(node: SchemaNode) {
18
+ const required = node.schema[KEYWORD];
19
+ if (required == null) {
20
+ return;
21
+ }
22
+
23
+ if (!isListOfStrings(required)) {
24
+ return node.createError("schema-error", {
25
+ pointer: `${node.schemaLocation}/${KEYWORD}`,
26
+ schema: node.schema,
27
+ value: required,
28
+ message: `Keyword '${KEYWORD}' must be a string[]`
29
+ });
30
+ }
31
+
32
+ node.required = required;
33
+ }
34
+
35
+ function validateRequired({ node, data, pointer = "#" }: JsonSchemaValidatorParams<"required">) {
36
+ const { required } = node;
14
37
  if (!isObject(data)) {
15
38
  return undefined;
16
39
  }
17
- return schema.required.map((property: string) => {
40
+ return required.map((property: string) => {
18
41
  if (!hasProperty(data, property)) {
19
42
  return node.createError("required-property-error", {
20
43
  key: property,
21
44
  pointer,
22
- schema,
45
+ schema: node.schema,
23
46
  value: data
24
47
  });
25
48
  }
@@ -2,17 +2,58 @@ import { getTypeOf, JSType } from "../utils/getTypeOf";
2
2
  import { SchemaNode } from "../types";
3
3
  import { Keyword, JsonSchemaReducerParams, JsonSchemaValidatorParams } from "../Keyword";
4
4
 
5
- export const typeKeyword: Keyword = {
6
- id: "type",
7
- keyword: "type",
8
- addReduce: (node) => Array.isArray(node.schema.type),
5
+ const KEYWORD = "type";
6
+ const validTyes = ["null", "boolean", "number", "integer", "string", "object", "array"];
7
+
8
+ export const typeKeyword: Keyword<"type"> = {
9
+ id: KEYWORD,
10
+ keyword: KEYWORD,
11
+ parse: parseType,
12
+ addReduce: (node) => Array.isArray(node.type),
9
13
  reduce: reduceType,
10
- addValidate: ({ schema }) => schema.type != null,
14
+ addValidate: (node) => node.type != null,
11
15
  validate: validateType
12
16
  };
13
17
 
18
+ function parseType(node: SchemaNode) {
19
+ const type = node.schema[KEYWORD];
20
+ if (type == null) {
21
+ return;
22
+ }
23
+ if (typeof type !== "string" && !Array.isArray(type)) {
24
+ return node.createError("schema-error", {
25
+ pointer: `${node.schemaLocation}/${KEYWORD}`,
26
+ schema: node.schema,
27
+ value: type,
28
+ message: `Keyword '${KEYWORD}' must be a string or a string[] - received ${typeof type}`
29
+ });
30
+ }
31
+ if (typeof type === "string" && !validTyes.includes(type)) {
32
+ return node.createError("schema-error", {
33
+ pointer: `${node.schemaLocation}/${KEYWORD}`,
34
+ schema: node.schema,
35
+ value: type,
36
+ message: `Keyword '${KEYWORD}' is not a valid JSON Schema type - received '${type}'. Expected one of ${validTyes.join(", ")}`
37
+ });
38
+ }
39
+
40
+ if (Array.isArray(type)) {
41
+ const invalidTypeIndex = type.findIndex((t) => !validTyes.includes(t));
42
+ if (invalidTypeIndex !== -1) {
43
+ return node.createError("schema-error", {
44
+ pointer: `${node.schemaLocation}/${KEYWORD}/${invalidTypeIndex}`,
45
+ schema: node.schema,
46
+ value: type[invalidTypeIndex],
47
+ message: `Keyword '${KEYWORD}' contains an invalid JSON Schema type: '${type[invalidTypeIndex]}'`
48
+ });
49
+ }
50
+ }
51
+
52
+ node[KEYWORD] = type;
53
+ }
54
+
14
55
  function reduceType({ node, pointer, data }: JsonSchemaReducerParams): undefined | SchemaNode {
15
- const dataType = getJsonSchemaType(data, node.schema.type);
56
+ const dataType = getJsonSchemaType(data, node.type!);
16
57
  if (dataType !== "undefined" && Array.isArray(node.schema.type) && node.schema.type.includes(dataType)) {
17
58
  return node.compileSchema({ ...node.schema, pointer, type: dataType }, node.evaluationPath);
18
59
  }
@@ -30,22 +71,18 @@ function getJsonSchemaType(value: unknown, expectedType: string | string[]): JST
30
71
  return jsType;
31
72
  }
32
73
 
33
- function validateType({ node, data, pointer }: JsonSchemaValidatorParams) {
34
- const schema = node.schema;
35
- const dataType = getJsonSchemaType(data, schema.type);
36
- if (
37
- data === undefined ||
38
- schema.type === dataType ||
39
- (Array.isArray(schema.type) && schema.type.includes(dataType))
40
- ) {
74
+ function validateType({ node, data, pointer }: JsonSchemaValidatorParams<"type">) {
75
+ const type = node[KEYWORD];
76
+ const dataType = getJsonSchemaType(data, type);
77
+ if (data === undefined || type === dataType || (Array.isArray(type) && type.includes(dataType))) {
41
78
  return;
42
79
  }
43
80
 
44
81
  return node.createError("type-error", {
45
82
  value: data,
46
83
  received: dataType,
47
- expected: schema.type,
48
- schema,
84
+ expected: type,
85
+ schema: node.schema,
49
86
  pointer
50
87
  });
51
88
  }
@@ -1,31 +1,47 @@
1
- import { isObject } from "../utils/isObject";
2
- import { SchemaNode } from "../types";
1
+ import { isBooleanSchema, isJsonSchema, SchemaNode } from "../types";
3
2
  import { Keyword, JsonSchemaValidatorParams, ValidationReturnType } from "../Keyword";
4
3
  import { validateNode } from "../validateNode";
5
4
  import { isItemEvaluated } from "../isItemEvaluated";
6
5
 
6
+ const KEYWORD = "unevaluatedItems";
7
+
7
8
  /**
8
9
  * @draft >= 2019-09
9
10
  * Similar to additionalItems, but can "see" into subschemas and across references
10
11
  * https://json-schema.org/draft/2019-09/json-schema-core#rfc.section.9.3.1.3
11
12
  */
12
13
  export const unevaluatedItemsKeyword: Keyword = {
13
- id: "unevaluatedItems",
14
- keyword: "unevaluatedItems",
14
+ id: KEYWORD,
15
+ keyword: KEYWORD,
15
16
  parse: parseUnevaluatedItems,
16
- addValidate: ({ schema }) => schema.unevaluatedItems != null,
17
+ addValidate: ({ schema }) => schema.unevaluatedItems != null, // currently we do not store boolean schema
17
18
  validate: validateUnevaluatedItems
18
19
  };
19
20
 
20
21
  export function parseUnevaluatedItems(node: SchemaNode) {
21
- if (!isObject(node.schema.unevaluatedItems)) {
22
+ const unevaluatedItems = node.schema[KEYWORD];
23
+ if (unevaluatedItems == null) {
24
+ return;
25
+ }
26
+ if (!(isJsonSchema(unevaluatedItems) || isBooleanSchema(unevaluatedItems))) {
27
+ return node.createError("schema-error", {
28
+ pointer: `${node.schemaLocation}/${KEYWORD}`,
29
+ schema: node.schema,
30
+ value: unevaluatedItems,
31
+ message: `Keyword '${KEYWORD}' must be a valid JSON Schema - received '${typeof unevaluatedItems}'`
32
+ });
33
+ }
34
+
35
+ if (isBooleanSchema(unevaluatedItems)) {
22
36
  return;
23
37
  }
38
+
24
39
  node.unevaluatedItems = node.compileSchema(
25
40
  node.schema.unevaluatedItems,
26
- `${node.evaluationPath}/unevaluatedItems`,
27
- `${node.schemaLocation}/unevaluatedItems`
41
+ `${node.evaluationPath}/${KEYWORD}`,
42
+ `${node.schemaLocation}/${KEYWORD}`
28
43
  );
44
+ return node.unevaluatedItems.schemaValidation;
29
45
  }
30
46
 
31
47
  function validateUnevaluatedItems({ node, data, pointer, path }: JsonSchemaValidatorParams) {
@@ -1,26 +1,43 @@
1
1
  import { isObject } from "../utils/isObject";
2
- import { SchemaNode } from "../types";
2
+ import { isBooleanSchema, isJsonSchema, SchemaNode } from "../types";
3
3
  import { Keyword, JsonSchemaValidatorParams, ValidationReturnType } from "../Keyword";
4
4
  import { validateNode } from "../validateNode";
5
5
  import { isPropertyEvaluated } from "../isPropertyEvaluated";
6
6
 
7
+ const KEYWORD = "unevaluatedProperties";
8
+
7
9
  export const unevaluatedPropertiesKeyword: Keyword = {
8
- id: "unevaluatedProperties",
9
- keyword: "unevaluatedProperties",
10
+ id: KEYWORD,
11
+ keyword: KEYWORD,
10
12
  parse: parseUnevaluatedProperties,
11
- addValidate: ({ schema }) => schema.unevaluatedProperties != null,
13
+ addValidate: ({ schema }) => schema[KEYWORD] != null, // currently we do not store boolean schema
12
14
  validate: validateUnevaluatedProperties
13
15
  };
14
16
 
15
17
  export function parseUnevaluatedProperties(node: SchemaNode) {
16
- if (!isObject(node.schema.unevaluatedProperties)) {
18
+ const unevaluatedProperties = node.schema[KEYWORD];
19
+ if (unevaluatedProperties == null) {
20
+ return;
21
+ }
22
+ if (!(isJsonSchema(unevaluatedProperties) || isBooleanSchema(unevaluatedProperties))) {
23
+ return node.createError("schema-error", {
24
+ pointer: `${node.schemaLocation}/${KEYWORD}`,
25
+ schema: node.schema,
26
+ value: unevaluatedProperties,
27
+ message: `Keyword '${KEYWORD}' must be a valid JSON Schema - received '${typeof unevaluatedProperties}'`
28
+ });
29
+ }
30
+
31
+ if (isBooleanSchema(unevaluatedProperties)) {
17
32
  return;
18
33
  }
34
+
19
35
  node.unevaluatedProperties = node.compileSchema(
20
36
  node.schema.unevaluatedProperties,
21
- `${node.evaluationPath}/unevaluatedProperties`,
22
- `${node.schemaLocation}/unevaluatedProperties`
37
+ `${node.evaluationPath}/${KEYWORD}`,
38
+ `${node.schemaLocation}/${KEYWORD}`
23
39
  );
40
+ return node.unevaluatedProperties.schemaValidation;
24
41
  }
25
42
 
26
43
  // @todo we should use collected annotation to evaluated unevaluate properties
@@ -1,14 +1,33 @@
1
- import { JsonError } from "../types";
1
+ import { JsonError, SchemaNode } from "../types";
2
2
  import { Keyword, JsonSchemaValidatorParams } from "../Keyword";
3
3
  import deepEqual from "fast-deep-equal";
4
4
 
5
+ const KEYWORD = "uniqueItems";
6
+
5
7
  export const uniqueItemsKeyword: Keyword = {
6
- id: "uniqueItems",
7
- keyword: "uniqueItems",
8
- addValidate: ({ schema }) => schema.uniqueItems === true,
8
+ id: KEYWORD,
9
+ keyword: KEYWORD,
10
+ parse: parseUniqueItems,
11
+ addValidate: ({ schema }) => schema[KEYWORD] === true,
9
12
  validate: validateUniqueItems
10
13
  };
11
14
 
15
+ function parseUniqueItems(node: SchemaNode) {
16
+ const uniqueItems = node.schema[KEYWORD];
17
+ if (uniqueItems == null || uniqueItems === false) {
18
+ return;
19
+ }
20
+ if (typeof uniqueItems !== "boolean") {
21
+ return node.createError("schema-error", {
22
+ pointer: `${node.schemaLocation}/${KEYWORD}`,
23
+ schema: node.schema,
24
+ value: uniqueItems,
25
+ message: `Keyword '${KEYWORD}' must be a boolean - received ${typeof uniqueItems}`
26
+ });
27
+ }
28
+ node.uniqueItems = true;
29
+ }
30
+
12
31
  function validateUniqueItems({ node, data, pointer }: JsonSchemaValidatorParams) {
13
32
  if (!Array.isArray(data)) {
14
33
  return undefined;
package/src/mergeNode.ts CHANGED
@@ -37,9 +37,12 @@ function mergeObjects(a?: Record<string, SchemaNode>, b?: Record<string, SchemaN
37
37
  return object;
38
38
  }
39
39
 
40
- // function mergeArray<T = unknown[]>(a?: T[], b?: T[]) {
41
- // return a || b ? [...(a ?? []), ...(b ?? [])] : undefined;
42
- // }
40
+ function combineArrays<T>(a?: T[], b?: T[]): T[] | undefined {
41
+ if (a == null || b == null) {
42
+ return b || a;
43
+ }
44
+ return a.concat(b).filter((value, index, list) => list.indexOf(value) === index);
45
+ }
43
46
 
44
47
  function mergePatternProperties(a?: SchemaNode["patternProperties"], b?: SchemaNode["patternProperties"]) {
45
48
  if (a == null || b == null) {
@@ -92,6 +95,7 @@ export function mergeNode(a?: SchemaNode, b?: SchemaNode, ...omit: string[]): Sc
92
95
 
93
96
  additionalProperties: mergeNode(a.additionalProperties, b.additionalProperties),
94
97
  contains: mergeNode(a.contains, b.contains),
98
+ enum: combineArrays(a.enum, b.enum),
95
99
  if: mergeNode(a.if, b.if),
96
100
  then: mergeNode(a.then, b.then),
97
101
  else: mergeNode(a.else, b.else),
@@ -101,7 +105,8 @@ export function mergeNode(a?: SchemaNode, b?: SchemaNode, ...omit: string[]): Sc
101
105
  unevaluatedItems: mergeNode(a.unevaluatedItems, b.unevaluatedItems),
102
106
  $defs: mergeObjects(a.$defs, b.$defs),
103
107
  patternProperties: mergePatternProperties(a.patternProperties, b.patternProperties),
104
- properties: mergeObjects(a.properties, b.properties)
108
+ properties: mergeObjects(a.properties, b.properties),
109
+ required: combineArrays(a.required, b.required)
105
110
  };
106
111
 
107
112
  // this removes any function that has no keyword associated on schema
package/src/settings.ts CHANGED
@@ -14,7 +14,8 @@ export default {
14
14
  "dependentRequired",
15
15
  "definitions",
16
16
  "dependencies",
17
- "patternProperties"
17
+ "patternProperties",
18
+ "propertyDependencies"
18
19
  ],
19
20
  REGEX_FLAGS: "u"
20
21
  };
package/src/types.ts CHANGED
@@ -67,5 +67,5 @@ export function isJsonError(error: unknown): error is JsonError {
67
67
  }
68
68
 
69
69
  export function isNumber(value: unknown): value is number {
70
- return isNaN(value as number) === false;
70
+ return typeof value === "number";
71
71
  }
@@ -0,0 +1,3 @@
1
+ export function isListOfStrings(v: unknown): v is string[] {
2
+ return Array.isArray(v) && v.find((item) => typeof item !== "string") == null;
3
+ }
@@ -8,7 +8,6 @@ describe("compileSchema : validate", () => {
8
8
  // note: boolean schema is already thoroughly tested by spec
9
9
  describe("boolean schema", () => {
10
10
  it("should fail if root schema is false", () => {
11
- // @ts-expect-error boolean typ unsupported
12
11
  const { errors } = compileSchema(false).validate("anything");
13
12
  assert.deepEqual(errors.length, 1);
14
13
  });
@@ -23,7 +22,6 @@ describe("compileSchema : validate", () => {
23
22
  assert.deepEqual(errors.length, 1);
24
23
  });
25
24
  it("should succeed if root schema is true", () => {
26
- // @ts-expect-error boolean typ unsupported
27
25
  const { errors } = compileSchema(true).validate("anything");
28
26
  assert.deepEqual(errors.length, 0);
29
27
  });
@@ -1,5 +1,5 @@
1
1
  import { BooleanSchema, JsonSchema, SchemaNode } from "./types";
2
- import { ValidationPath, ValidationReturnType } from "./Keyword";
2
+ import { SchemaNodeWithRequired, ValidationPath, ValidationReturnType } from "./Keyword";
3
3
  import sanitizeErrors from "./utils/sanitizeErrors";
4
4
 
5
5
  export function validateNode(node: SchemaNode, data: unknown, pointer: string, path: ValidationPath) {
@@ -19,7 +19,7 @@ export function validateNode(node: SchemaNode, data: unknown, pointer: string, p
19
19
  }
20
20
  const errors: ValidationReturnType = [];
21
21
  for (const validate of node.validators) {
22
- const result = validate({ node, data, pointer, path });
22
+ const result = validate({ node: node as SchemaNodeWithRequired<keyof SchemaNode>, data, pointer, path });
23
23
  if (Array.isArray(result)) {
24
24
  errors.push(...result);
25
25
  } else if (result) {