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
package/index.ts CHANGED
@@ -3,7 +3,15 @@ export type { CompileOptions } from "./src/compileSchema";
3
3
  export type { Context, SchemaNode, GetNodeOptions, ValidateReturnType } from "./src/SchemaNode";
4
4
  export type { DataNode } from "./src/methods/toDataNodes";
5
5
  export type { Draft, DraftVersion } from "./src/Draft";
6
- export type { JsonError, JsonAnnotation, JsonPointer, JsonSchema, BooleanSchema, OptionalNodeOrError, NodeOrError } from "./src/types";
6
+ export type {
7
+ JsonError,
8
+ JsonAnnotation,
9
+ JsonPointer,
10
+ JsonSchema,
11
+ BooleanSchema,
12
+ OptionalNodeOrError,
13
+ NodeOrError
14
+ } from "./src/types";
7
15
  export type {
8
16
  Keyword,
9
17
  Maybe,
@@ -29,6 +37,7 @@ export { draftEditor } from "./src/draftEditor";
29
37
 
30
38
  // keywords
31
39
  export { oneOfFuzzyKeyword, oneOfKeyword } from "./src/keywords/oneOf";
40
+ export { propertyDependenciesKeyword } from "./src/keywords/propertyDependencies";
32
41
 
33
42
  // errors
34
43
  export { render } from "./src/errors/render";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "json-schema-library",
3
- "version": "11.0.4",
3
+ "version": "11.1.0",
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",
@@ -13,7 +13,6 @@
13
13
  "./package.json": "./package.json"
14
14
  },
15
15
  "scripts": {
16
- "preinstall": "npx only-allow yarn",
17
16
  "coverage": "nyc npm run test --reporter=lcov",
18
17
  "dist": "tsdown -f esm -f cjs --minify; yarn dist:iife",
19
18
  "dist:iife": "tsdown --config tsdown.iife.config.ts --no-clean --minify; mv dist/index.iife.js dist/jlib.js",
@@ -66,19 +65,19 @@
66
65
  "homepage": "https://github.com/sagold/json-schema-library",
67
66
  "devDependencies": {
68
67
  "@eslint/js": "^10.0.1",
69
- "@types/glob": "^9.0.0",
68
+ "@types/glob": "^ 8.1.0",
70
69
  "@types/mocha": "^10.0.6",
71
- "@types/node": "^25.2.3",
70
+ "@types/node": "^25.3.3",
72
71
  "@types/valid-url": "^1.0.7",
73
72
  "eslint": "^9.39.2",
74
- "glob": "^13.0.3",
73
+ "glob": "^13.0.6",
75
74
  "json-schema-test-suite": "https://github.com/json-schema-org/JSON-Schema-Test-Suite#Test-JSON-Schema-Acceptance-1.029",
76
75
  "mocha": "^11.7.5",
77
76
  "nyc": "^17.1.0",
78
- "tsdown": "^0.20.3",
77
+ "tsdown": "^0.21.2",
79
78
  "tsx": "^4.21.0",
80
79
  "typescript": "^5.9.3",
81
- "typescript-eslint": "^8.55.0",
80
+ "typescript-eslint": "^8.57.0",
82
81
  "watch": "^1.0.1"
83
82
  },
84
83
  "dependencies": {
@@ -93,14 +92,18 @@
93
92
  "valid-url": "^1.0.9"
94
93
  },
95
94
  "resolutions": {
95
+ "ajv": ">=6.14.0",
96
96
  "braces": ">=3.0.3",
97
97
  "cross-spawn": ">=7.0.6",
98
+ "diff": ">=8.0.3",
98
99
  "json5": ">=2.2.3",
99
100
  "js-yaml": ">=4.1.1",
100
101
  "lodash": ">=4",
101
102
  "merge": ">=2",
102
103
  "micromatch": ">=4.0.8",
103
- "string-width": ">=4.2.3"
104
+ "serialize-javascript": ">=7.0.3",
105
+ "string-width": ">=4.2.3",
106
+ "minimatch": ">=9.0.7"
104
107
  },
105
108
  "publishConfig": {
106
109
  "registry": "https://registry.npmjs.org"
package/src/Draft.ts CHANGED
@@ -2,7 +2,7 @@ import type { JsonSchemaValidator, Keyword } from "./Keyword";
2
2
  import { copyDraft } from "./utils/copyDraft";
3
3
  import { createSchema } from "./methods/createSchema";
4
4
  import { toDataNodes } from "./methods/toDataNodes";
5
- import { ErrorConfig } from "./types";
5
+ import { ErrorConfig, SchemaNode } from "./types";
6
6
  import { getChildSelection } from "./methods/getChildSelection";
7
7
  import { getData } from "./methods/getData";
8
8
 
package/src/Keyword.ts CHANGED
@@ -37,30 +37,56 @@ export type ValidationAnnotation = JsonError | JsonAnnotation | Promise<Maybe<Va
37
37
  type ValidationResult = Maybe<ValidationAnnotation>;
38
38
  export type ValidationReturnType = ValidationResult | ValidationResult[];
39
39
 
40
- export type JsonSchemaValidatorParams = { pointer: string; data: unknown; node: SchemaNode; path: ValidationPath };
41
- export interface JsonSchemaValidator {
40
+ export type SchemaNodeWithRequired<K extends keyof SchemaNode> = SchemaNode & Required<Pick<SchemaNode, K>>;
41
+ export type JsonSchemaValidatorParams<Key extends keyof SchemaNode = keyof SchemaNode> = {
42
+ pointer: string;
43
+ data: unknown;
44
+ node: SchemaNodeWithRequired<Key>;
45
+ path: ValidationPath;
46
+ };
47
+ export interface JsonSchemaValidator<Key extends keyof SchemaNode = keyof SchemaNode> {
42
48
  toJSON?: () => string;
43
49
  order?: number;
44
- (options: JsonSchemaValidatorParams): ValidationReturnType;
50
+ (options: JsonSchemaValidatorParams<Key>): ValidationReturnType;
45
51
  }
46
52
 
47
- export type Keyword = {
53
+ export type Keyword<Key extends keyof SchemaNode = keyof SchemaNode> = {
48
54
  id: string;
49
55
  /** unique keyword corresponding to JSON Schema keywords (or custom) */
50
56
  keyword: string;
51
57
  /** sort order of keyword. Lower numbers will be processed last. Default is 0 */
52
58
  order?: number;
53
- /** called with compileSchema */
54
- parse?: (node: SchemaNode) => void;
59
+ /**
60
+ * Called once for each JSON Schema dduring compileSchema to evaluate keyword.
61
+ * Use this to skip or preprocess the Keyword for the given JSON Schema and
62
+ * to create any schema annotations, like input errors.
63
+ *
64
+ * - most keywords cache their evaluation directly on node, e.g. node.required
65
+ * - most keywords skip any other actions if their evaluation is missing on node
66
+ * - return any errors found in JSON schema related to this keyword
67
+ * (this includes errors from created nodes)
68
+ */
69
+ parse?: (node: SchemaNode) => void | ValidationAnnotation | ValidationAnnotation[];
55
70
  addResolve?: (node: SchemaNode) => boolean;
56
- /** return node corresponding to passed in key or do nothing */
71
+ /**
72
+ * If this contains child-data, resolve must return schema associated for the passed in key
73
+ *
74
+ * @example
75
+ * a keyword properties has has child-properties. So when a properties[key] exists,
76
+ * it will return the node of properties[key] or nothing at all
77
+ */
57
78
  resolve?: JsonSchemaResolver;
58
79
 
80
+ /** return true if the given node should run the validate-function on this keyword */
59
81
  addValidate?: (node: SchemaNode) => boolean;
60
- /** validate data using node */
61
- validate?: JsonSchemaValidator;
82
+ /**
83
+ * Perform validation for this keyword and the passed in data
84
+ */
85
+ validate?: JsonSchemaValidator<Key>;
62
86
 
63
87
  addReduce?: (node: SchemaNode) => boolean;
64
- /** remove dynamic schema-keywords by merging valid sub-schemas */
88
+ /**
89
+ * Remove dynamic schema-keywords by merging valid sub-schemas
90
+ */
65
91
  reduce?: JsonSchemaReducer;
66
92
  };
package/src/SchemaNode.ts CHANGED
@@ -24,7 +24,8 @@ import {
24
24
  OptionalNodeOrError,
25
25
  NodeOrError,
26
26
  JsonAnnotation,
27
- isJsonAnnotation
27
+ isJsonAnnotation,
28
+ isBooleanSchema
28
29
  } from "./types";
29
30
  import { isObject } from "./utils/isObject";
30
31
  import { join } from "@sagold/json-pointer";
@@ -91,6 +92,8 @@ export type Context = {
91
92
  formats: Draft["formats"];
92
93
  /** [SHARED USING ADD REMOTE] getData default options */
93
94
  getDataDefaultOptions?: TemplateOptions;
95
+ /** [SHARED USING ADD REMOTE] collect unknown keywords in schemaAnnotations */
96
+ withSchemaAnnotations?: boolean;
94
97
  };
95
98
 
96
99
  export interface SchemaNode extends SchemaNodeMethodsType {
@@ -129,6 +132,7 @@ export interface SchemaNode extends SchemaNodeMethodsType {
129
132
  reducers: JsonSchemaReducer[];
130
133
  resolvers: JsonSchemaResolver[];
131
134
  validators: JsonSchemaValidator[];
135
+ schemaValidation?: ValidationAnnotation[];
132
136
 
133
137
  // parsed schema properties (registered by parsers)
134
138
  $id?: string;
@@ -140,7 +144,9 @@ export interface SchemaNode extends SchemaNodeMethodsType {
140
144
  contains?: SchemaNode;
141
145
  dependentRequired?: Record<string, string[]>;
142
146
  dependentSchemas?: Record<string, SchemaNode | boolean>;
147
+ deprecated?: boolean;
143
148
  else?: SchemaNode;
149
+ enum?: unknown[];
144
150
  if?: SchemaNode;
145
151
  /**
146
152
  * # Items-array schema - for all drafts
@@ -177,21 +183,40 @@ export interface SchemaNode extends SchemaNodeMethodsType {
177
183
  * | [AdditionalItems Specification](https://json-schema.org/draft/2019-09/draft-handrews-json-schema-02#additionalItems)
178
184
  */
179
185
  items?: SchemaNode;
186
+ maximum?: number;
187
+ minimum?: number;
188
+ maxItems?: number;
189
+ maxLength?: number;
190
+ maxProperties?: number;
191
+ minItems?: number;
192
+ minLength?: number;
193
+ minProperties?: number;
180
194
  not?: SchemaNode;
181
195
  oneOf?: SchemaNode[];
196
+ multipleOf?: number;
197
+ pattern?: RegExp;
182
198
  patternProperties?: { name: string; pattern: RegExp; node: SchemaNode }[];
199
+ propertyDependencies?: Record<string, Record<string, SchemaNode>>;
183
200
  properties?: Record<string, SchemaNode>;
184
201
  propertyNames?: SchemaNode;
202
+ required?: string[];
185
203
  then?: SchemaNode;
204
+ type?: string | string[];
186
205
  unevaluatedItems?: SchemaNode;
187
206
  unevaluatedProperties?: SchemaNode;
207
+ uniqueItems?: true;
188
208
  }
189
209
 
190
210
  /**
191
211
  * Fixed SchemaNode mixin methods
192
212
  */
193
213
  interface SchemaNodeMethodsType {
194
- compileSchema(schema: JsonSchema | BooleanSchema, evaluationPath?: string, schemaLocation?: string, dynamicId?: string): SchemaNode;
214
+ compileSchema(
215
+ schema: JsonSchema | BooleanSchema,
216
+ evaluationPath?: string,
217
+ schemaLocation?: string,
218
+ dynamicId?: string
219
+ ): SchemaNode;
195
220
  createError<T extends string = DefaultErrors>(code: T, data: AnnotationData, message?: string): JsonError;
196
221
  createAnnotation<T extends string = DefaultErrors>(code: T, data: AnnotationData, message?: string): JsonAnnotation;
197
222
  createSchema(data?: unknown): JsonSchema;
@@ -325,7 +350,20 @@ export const SchemaNodeMethods = {
325
350
  ...SchemaNodeMethods
326
351
  };
327
352
 
328
- addKeywords(node);
353
+ if (!isJsonSchema(schema) && !isBooleanSchema(schema)) {
354
+ node.schemaValidation = [
355
+ node.createError("schema-error", {
356
+ pointer: schemaLocation ?? evaluationPath,
357
+ schema,
358
+ value: undefined,
359
+ message: `JSON schema must be object or boolean - reveived: '${schema}'`
360
+ })
361
+ ];
362
+ return node;
363
+ }
364
+ const schemaValidation = addKeywords(node).filter((err) => err != null);
365
+ node.schemaValidation = sanitizeErrors(schemaValidation);
366
+
329
367
  return node;
330
368
  },
331
369
 
@@ -497,15 +535,15 @@ export const SchemaNodeMethods = {
497
535
  * @returns the current node (not the remote schema-node)
498
536
  */
499
537
  addRemoteSchema(url: string, schema: JsonSchema | BooleanSchema): SchemaNode {
500
- // @draft >= 6
501
- if (isJsonSchema(schema)) {
502
- schema.$id = resolveUri(schema.$id || url);
503
- }
504
-
538
+ // @draft >= 6
539
+ if (isJsonSchema(schema)) {
540
+ schema.$id = resolveUri(schema.$id || url);
541
+ }
542
+
505
543
  const node = this as SchemaNode;
506
544
  const { context } = node;
507
545
  const schemaId = isJsonSchema(schema) ? schema.$schema : undefined;
508
- const draft = getDraft(context.drafts, schemaId ?? context.rootNode.schema?.$schema);
546
+ const draft = getDraft(context.drafts, schemaId ?? context.rootNode.schema?.$schema);
509
547
 
510
548
  const remoteNode: SchemaNode = {
511
549
  evaluationPath: "#",
@@ -565,20 +603,40 @@ const noRefMergeDrafts = ["draft-04", "draft-06", "draft-07"];
565
603
  export function addKeywords(node: SchemaNode) {
566
604
  if (node.schema.$ref && noRefMergeDrafts.includes(node.context.version)) {
567
605
  // for these draft versions only ref is validated
568
- node.context.keywords
606
+ return node.context.keywords
569
607
  .filter(({ keyword }) => whitelist.includes(keyword))
570
- .forEach((keyword) => execKeyword(keyword, node));
571
- return;
608
+ .map((keyword) => execKeyword(keyword, node));
572
609
  }
573
610
  const keys = Object.keys(node.schema);
574
- node.context.keywords
575
- .filter(({ keyword }) => keys.includes(keyword) || whitelist.includes(keyword))
576
- .forEach((keyword) => execKeyword(keyword, node));
611
+ const errors = node.context.keywords
612
+ .filter(({ keyword }) => whitelist.includes(keyword) || keys.includes(keyword))
613
+ .map((keyword) => execKeyword(keyword, node));
614
+
615
+ // find unused keywords
616
+ if (node.context.withSchemaAnnotations) {
617
+ Object.keys(node.schema)
618
+ .filter(
619
+ (key) =>
620
+ !key.startsWith("x-") && node.context.keywords.find((keyword) => keyword.keyword === key) == null
621
+ )
622
+ .forEach((keyword) => {
623
+ errors.push(
624
+ node.createAnnotation("unknown-keyword-warning", {
625
+ pointer: `${node.schemaLocation}/${keyword}`,
626
+ schema: node.schema,
627
+ value: keyword,
628
+ draft: node.getDraftVersion()
629
+ })
630
+ );
631
+ });
632
+ }
633
+
634
+ return errors;
577
635
  }
578
636
 
579
637
  export function execKeyword(keyword: Keyword, node: SchemaNode) {
580
638
  // @todo consider first parsing all nodes
581
- keyword.parse?.(node);
639
+ const errors = keyword.parse?.(node);
582
640
  if (keyword.reduce && keyword.addReduce?.(node)) {
583
641
  node.reducers.push(keyword.reduce);
584
642
  }
@@ -588,4 +646,5 @@ export function execKeyword(keyword: Keyword, node: SchemaNode) {
588
646
  if (keyword.validate && keyword.addValidate?.(node)) {
589
647
  node.validators.push(keyword.validate);
590
648
  }
649
+ return errors;
591
650
  }
@@ -6,10 +6,21 @@ import { draft07 } from "./draft07";
6
6
  import { draft2019 } from "./draft2019";
7
7
  import { draft2020 } from "./draft2020";
8
8
  import { pick } from "./utils/pick";
9
- import { JsonSchema, BooleanSchema, Draft, isJsonSchema } from "./types";
9
+ import {
10
+ JsonSchema,
11
+ BooleanSchema,
12
+ Draft,
13
+ isJsonSchema,
14
+ JsonAnnotation,
15
+ JsonError,
16
+ isJsonError,
17
+ isJsonAnnotation,
18
+ isBooleanSchema
19
+ } from "./types";
10
20
  import { TemplateOptions } from "./methods/getData";
11
21
  import { SchemaNode, SchemaNodeMethods, addKeywords, isSchemaNode } from "./SchemaNode";
12
22
  import settings from "./settings";
23
+ import sanitizeErrors from "./utils/sanitizeErrors";
13
24
 
14
25
  const { REGEX_FLAGS } = settings;
15
26
 
@@ -18,6 +29,10 @@ export type CompileOptions = {
18
29
  remote?: SchemaNode;
19
30
  formatAssertion?: boolean | "meta-schema" | undefined;
20
31
  getDataDefaultOptions?: TemplateOptions;
32
+ /** set to true to throw an Errors on errors in input schema. Defaults to false */
33
+ throwOnInvalidSchema?: boolean;
34
+ /** set to true to collect unknown keywords of input schema in `node.schemaAnnotations`. Defaults to false */
35
+ withSchemaAnnotations?: boolean;
21
36
  };
22
37
 
23
38
  const defaultDrafts: Draft[] = [draft04, draft06, draft07, draft2019, draft2020];
@@ -35,8 +50,7 @@ export function compileSchema(schema: JsonSchema | BooleanSchema, options: Compi
35
50
  let formatAssertion = options.formatAssertion ?? true;
36
51
  const drafts = options.drafts ?? defaultDrafts;
37
52
  const draft = getDraft(drafts, isJsonSchema(schema) ? schema.$schema : undefined);
38
-
39
- const node: SchemaNode = {
53
+ const node: SchemaNode & { schemaErrors?: JsonError[]; schemaAnnotations: JsonAnnotation[] } = {
40
54
  evaluationPath: "#",
41
55
  lastIdPointer: "#",
42
56
  schemaLocation: "#",
@@ -54,6 +68,7 @@ export function compileSchema(schema: JsonSchema | BooleanSchema, options: Compi
54
68
  refs: {},
55
69
  ...copy(pick(draft, "methods", "keywords", "version", "formats", "errors")),
56
70
  getDataDefaultOptions: options.getDataDefaultOptions,
71
+ withSchemaAnnotations: options.withSchemaAnnotations ?? false,
57
72
  drafts
58
73
  },
59
74
  ...SchemaNodeMethods
@@ -82,6 +97,40 @@ export function compileSchema(schema: JsonSchema | BooleanSchema, options: Compi
82
97
  node.context.keywords = node.context.keywords.filter((f) => f.keyword !== "format");
83
98
  }
84
99
 
85
- addKeywords(node);
100
+ if (!isJsonSchema(schema) && !isBooleanSchema(schema)) {
101
+ node.schemaErrors = [
102
+ node.createError("schema-error", {
103
+ pointer: "#",
104
+ schema,
105
+ value: undefined,
106
+ message: `JSON schema must be object or boolean - reveived: '${schema}'`
107
+ })
108
+ ];
109
+ return node;
110
+ }
111
+
112
+ // parse and validate schema
113
+ let schemaValidation = addKeywords(node).filter((err) => err != null);
114
+ schemaValidation = sanitizeErrors(schemaValidation);
115
+ const schemaErrors: JsonError[] = [];
116
+ const schemaAnnotations: JsonAnnotation[] = [];
117
+ schemaValidation.forEach((error) => {
118
+ if (isJsonError(error)) {
119
+ schemaErrors.push(error);
120
+ } else if (isJsonAnnotation(error)) {
121
+ schemaAnnotations.push(error);
122
+ }
123
+ });
124
+
125
+ if (options.throwOnInvalidSchema && schemaErrors.length > 0) {
126
+ const error = new Error("Invalid schema passed to compileSchema");
127
+ // @ts-expect-error unknown error-property
128
+ error.data = schemaErrors;
129
+ throw error;
130
+ }
131
+
132
+ node.schemaErrors = schemaErrors;
133
+ node.schemaAnnotations = schemaAnnotations;
134
+
86
135
  return node;
87
136
  }
@@ -74,5 +74,8 @@ export const errors = {
74
74
  "unknown-property-error": "Could not find a valid schema for property `{{pointer}}` within object",
75
75
  "value-not-empty-error": "A value for `{{property}}` is required at `{{pointer}}`",
76
76
  // annotations
77
- "deprecated-warning": "Value at `{{pointer}}` is deprecated"
77
+ "deprecated-warning": "Value at `{{pointer}}` is deprecated",
78
+ // schema validation
79
+ "schema-error": "Invalid schema found at {{pointer}}: {{message}}",
80
+ "unknown-keyword-warning": "Keyword '{{value}}' is not a valid keyword to draft '{{draft}}'"
78
81
  };
@@ -1,5 +1,6 @@
1
- import { Keyword } from "../Keyword";
1
+ import { Keyword, ValidationAnnotation } from "../Keyword";
2
2
  import { SchemaNode } from "../types";
3
+ import { isObject } from "../utils/isObject";
3
4
 
4
5
  export const $defsKeyword: Keyword = {
5
6
  id: "$defs",
@@ -8,17 +9,40 @@ export const $defsKeyword: Keyword = {
8
9
  };
9
10
 
10
11
  export function parseDefs(node: SchemaNode) {
12
+ const errors: ValidationAnnotation[] = [];
13
+
11
14
  if (node.schema.$defs) {
12
- node.$defs = node.$defs ?? {};
13
- Object.keys(node.schema.$defs).forEach((property) => {
14
- node.$defs![property] = node.compileSchema(
15
- node.schema.$defs[property],
16
- `${node.evaluationPath}/$defs/${urlEncodeJsonPointerProperty(property)}`,
17
- `${node.schemaLocation}/$defs/${property}`
15
+ if (!isObject(node.schema.$defs)) {
16
+ errors.push(
17
+ node.createError("schema-error", {
18
+ pointer: node.schemaLocation,
19
+ schema: node.schema,
20
+ value: node.schema.$defs,
21
+ message: `$defs must be an object - received: ${typeof node.schema.$defs}`
22
+ })
18
23
  );
19
- });
24
+ } else {
25
+ node.$defs = node.$defs ?? {};
26
+ Object.keys(node.schema.$defs).forEach((property) => {
27
+ node.$defs![property] = node.compileSchema(
28
+ node.schema.$defs[property],
29
+ `${node.evaluationPath}/$defs/${urlEncodeJsonPointerProperty(property)}`,
30
+ `${node.schemaLocation}/$defs/${property}`
31
+ );
32
+ });
33
+ }
20
34
  }
21
35
  if (node.schema.definitions) {
36
+ if (!isObject(node.schema.definitions)) {
37
+ errors.push(
38
+ node.createError("schema-error", {
39
+ pointer: node.schemaLocation,
40
+ schema: node.schema,
41
+ value: node.schema.$defs,
42
+ message: `definitions must be an object - received: ${typeof node.schema.definitions}`
43
+ })
44
+ );
45
+ }
22
46
  node.$defs = node.$defs ?? {};
23
47
  Object.keys(node.schema.definitions).forEach((property) => {
24
48
  node.$defs![property] = node.compileSchema(
@@ -28,6 +52,8 @@ export function parseDefs(node: SchemaNode) {
28
52
  );
29
53
  });
30
54
  }
55
+
56
+ return errors;
31
57
  }
32
58
 
33
59
  function urlEncodeJsonPointerProperty(property: string) {
@@ -72,6 +72,18 @@ export function parseRef(node: SchemaNode) {
72
72
  node.$ref = `#${node.$ref}`;
73
73
  }
74
74
  }
75
+
76
+ // validate simple ref to definitions
77
+ if (node.$ref?.startsWith("#/$defs/")) {
78
+ if (get(node.getNodeRoot().schema, node.$ref) == null) {
79
+ return node.createError("schema-error", {
80
+ pointer: `${node.schemaLocation}/$ref`,
81
+ schema: node.schema,
82
+ value: node.schema.$ref,
83
+ message: `Invalid $ref to missing target '${node.schema.ref}'`
84
+ });
85
+ }
86
+ }
75
87
  }
76
88
 
77
89
  export function reduceRef({ node, data, key, pointer, path }: JsonSchemaReducerParams) {
@@ -1,7 +1,7 @@
1
1
  import settings from "../settings";
2
2
  import { isObject } from "../utils/isObject";
3
- import { Keyword, JsonSchemaResolverParams, JsonSchemaValidatorParams, ValidationReturnType} from "../Keyword";
4
- import { SchemaNode } from "../types";
3
+ import { Keyword, JsonSchemaResolverParams, JsonSchemaValidatorParams, ValidationReturnType } from "../Keyword";
4
+ import { isBooleanSchema, SchemaNode } from "../types";
5
5
  import { getValue } from "../utils/getValue";
6
6
  import { validateNode } from "../validateNode";
7
7
 
@@ -24,13 +24,24 @@ export const additionalPropertiesKeyword: Keyword = {
24
24
  // must come as last resolver
25
25
  export function parseAdditionalProperties(node: SchemaNode) {
26
26
  const { schema, evaluationPath, schemaLocation } = node;
27
- if (isObject(schema.additionalProperties)) {
28
- node.additionalProperties = node.compileSchema(
29
- schema.additionalProperties,
30
- `${evaluationPath}/additionalProperties`,
31
- `${schemaLocation}/additionalProperties`
32
- );
27
+ if (schema.additionalProperties == null || isBooleanSchema(schema.additionalProperties)) {
28
+ return;
33
29
  }
30
+
31
+ if (!isObject(schema.additionalProperties)) {
32
+ return node.createError("schema-error", {
33
+ pointer: node.schemaLocation,
34
+ schema,
35
+ value: schema.additionalProperties,
36
+ message: `keyword 'additionalProperties' must be a valid JSON Schema - receoved: ${typeof schema.additionalProperties}`
37
+ });
38
+ }
39
+
40
+ node.additionalProperties = node.compileSchema(
41
+ schema.additionalProperties,
42
+ `${evaluationPath}/additionalProperties`,
43
+ `${schemaLocation}/additionalProperties`
44
+ );
34
45
  }
35
46
 
36
47
  function additionalPropertyResolver({ node, data, key }: JsonSchemaResolverParams) {
@@ -1,29 +1,55 @@
1
1
  import { mergeSchema } from "../utils/mergeSchema";
2
- import { Keyword, JsonSchemaReducerParams, JsonSchemaValidatorParams, ValidationReturnType } from "../Keyword";
2
+ import {
3
+ Keyword,
4
+ JsonSchemaReducerParams,
5
+ JsonSchemaValidatorParams,
6
+ ValidationReturnType,
7
+ ValidationAnnotation
8
+ } from "../Keyword";
3
9
  import { SchemaNode } from "../types";
4
10
  import { validateNode } from "../validateNode";
5
11
 
12
+ const KEYWORD = "allOf";
13
+
6
14
  export const allOfKeyword: Keyword = {
7
- id: "allOf",
8
- keyword: "allOf",
15
+ id: KEYWORD,
16
+ keyword: KEYWORD,
9
17
  parse: parseAllOf,
10
- addReduce: (node: SchemaNode) => node.allOf != null,
18
+ addReduce: (node: SchemaNode) => node[KEYWORD] != null,
11
19
  reduce: reduceAllOf,
12
- addValidate: (node) => node.allOf != null,
20
+ addValidate: (node) => node[KEYWORD] != null,
13
21
  validate: validateAllOf
14
22
  };
15
23
 
16
24
  export function parseAllOf(node: SchemaNode) {
17
- const { schema, evaluationPath } = node;
18
- if (Array.isArray(schema.allOf) && schema.allOf.length) {
19
- node.allOf = schema.allOf.map((s, index) =>
20
- node.compileSchema(s, `${evaluationPath}/allOf/${index}`, `${node.schemaLocation}/allOf/${index}`)
21
- );
25
+ const { schema, evaluationPath, schemaLocation } = node;
26
+ if (schema[KEYWORD] == null) {
27
+ return;
28
+ }
29
+ if (!Array.isArray(schema[KEYWORD])) {
30
+ return node.createError("schema-error", {
31
+ pointer: schemaLocation,
32
+ schema,
33
+ value: schema[KEYWORD],
34
+ message: `Keyword '${KEYWORD}' must be an array - received '${typeof schema[KEYWORD]}'`
35
+ });
36
+ }
37
+ if (schema[KEYWORD].length === 0) {
38
+ return;
22
39
  }
40
+
41
+ node[KEYWORD] = schema[KEYWORD].map((s, index) =>
42
+ node.compileSchema(s, `${evaluationPath}/${KEYWORD}/${index}`, `${schemaLocation}/${KEYWORD}/${index}`)
43
+ );
44
+
45
+ return node[KEYWORD].reduce((errors, node) => {
46
+ if (node.schemaValidation) errors.push(...node.schemaValidation);
47
+ return errors;
48
+ }, [] as ValidationAnnotation[]);
23
49
  }
24
50
 
25
51
  function reduceAllOf({ node, data, key, pointer, path }: JsonSchemaReducerParams) {
26
- if (node.allOf == null) {
52
+ if (node[KEYWORD] == null) {
27
53
  return;
28
54
  }
29
55
 
@@ -31,15 +57,15 @@ function reduceAllOf({ node, data, key, pointer, path }: JsonSchemaReducerParams
31
57
  // dynamic schema parts
32
58
  let mergedSchema = {};
33
59
  let dynamicId = "";
34
- for (let i = 0; i < node.allOf.length; i += 1) {
35
- const { node: schemaNode } = node.allOf[i].reduceNode(data, { key, pointer, path });
60
+ for (let i = 0; i < node[KEYWORD].length; i += 1) {
61
+ const { node: schemaNode } = node[KEYWORD][i].reduceNode(data, { key, pointer, path });
36
62
  if (schemaNode) {
37
63
  const nestedDynamicId = schemaNode.dynamicId?.replace(node.dynamicId, "") ?? "";
38
- const localDynamicId = nestedDynamicId === "" ? `allOf/${i}` : nestedDynamicId;
64
+ const localDynamicId = nestedDynamicId === "" ? `${KEYWORD}/${i}` : nestedDynamicId;
39
65
  dynamicId += `${dynamicId === "" ? "" : ","}${localDynamicId}`;
40
66
 
41
- const schema = mergeSchema(node.allOf[i].schema, schemaNode.schema);
42
- mergedSchema = mergeSchema(mergedSchema, schema, "allOf", "contains");
67
+ const schema = mergeSchema(node[KEYWORD][i].schema, schemaNode.schema);
68
+ mergedSchema = mergeSchema(mergedSchema, schema, KEYWORD, "contains");
43
69
  }
44
70
  }
45
71
 
@@ -52,11 +78,11 @@ function reduceAllOf({ node, data, key, pointer, path }: JsonSchemaReducerParams
52
78
  }
53
79
 
54
80
  function validateAllOf({ node, data, pointer, path }: JsonSchemaValidatorParams) {
55
- if (!Array.isArray(node.allOf) || node.allOf.length === 0) {
81
+ if (!Array.isArray(node[KEYWORD]) || node[KEYWORD].length === 0) {
56
82
  return;
57
83
  }
58
84
  const errors: ValidationReturnType = [];
59
- node.allOf.forEach((allOfNode) => {
85
+ node[KEYWORD].forEach((allOfNode) => {
60
86
  errors.push(...validateNode(allOfNode, data, pointer, path));
61
87
  });
62
88
  return errors;