json-schema-library 11.0.5 → 11.2.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 (113) hide show
  1. package/.mocharc.js +1 -0
  2. package/CHANGELOG.md +17 -0
  3. package/README.md +120 -0
  4. package/bowtie/.editorconfig +21 -0
  5. package/bowtie/.prettierrc +6 -0
  6. package/bowtie/BOWTIE.md +54 -0
  7. package/bowtie/Dockerfile +6 -0
  8. package/bowtie/bowtie-api.ts +101 -0
  9. package/bowtie/bowtie.test.ts +76 -0
  10. package/bowtie/bowtie.ts +156 -0
  11. package/bowtie/package.json +11 -0
  12. package/bowtie/tsconfig.json +12 -0
  13. package/dist/index.cjs +1 -1
  14. package/dist/index.d.cts +39 -470
  15. package/dist/index.d.mts +39 -470
  16. package/dist/index.mjs +1 -1
  17. package/dist/jlib.js +2 -13
  18. package/dist/remotes/index.cjs +1 -0
  19. package/dist/remotes/index.d.cts +7 -0
  20. package/dist/remotes/index.d.mts +7 -0
  21. package/dist/remotes/index.mjs +1 -0
  22. package/dist/types-B2wwNWyo.d.cts +513 -0
  23. package/dist/types-BhTU1l2h.d.mts +513 -0
  24. package/index.ts +10 -4
  25. package/package.json +14 -8
  26. package/src/Keyword.ts +37 -12
  27. package/src/SchemaNode.ts +84 -16
  28. package/src/compileSchema.ts +56 -4
  29. package/src/draft04/keywords/$ref.ts +22 -14
  30. package/src/draft04/keywords/exclusiveMaximum.ts +14 -0
  31. package/src/draft04/keywords/exclusiveMinimum.ts +14 -0
  32. package/src/draft04/validateSchema.test.ts +20 -0
  33. package/src/draft06/keywords/$ref.ts +15 -5
  34. package/src/draft2019-09/keywords/$ref.test.ts +3 -1
  35. package/src/draft2019-09/keywords/$ref.ts +40 -16
  36. package/src/draft2019-09/keywords/additionalItems.ts +33 -10
  37. package/src/draft2019-09/keywords/items.ts +32 -10
  38. package/src/draft2019-09/keywords/unevaluatedItems.ts +19 -6
  39. package/src/draft2019-09/methods/getData.ts +1 -1
  40. package/src/draft2019-09/validateSchema.test.ts +28 -0
  41. package/src/errors/errors.ts +8 -1
  42. package/src/formats/formats.ts +35 -28
  43. package/src/formats/hyperjump.d.ts +172 -0
  44. package/src/keywords/$defs.ts +34 -8
  45. package/src/keywords/$ref.ts +59 -13
  46. package/src/keywords/additionalProperties.ts +19 -8
  47. package/src/keywords/allOf.ts +44 -18
  48. package/src/keywords/anyOf.ts +38 -19
  49. package/src/keywords/contains.ts +21 -9
  50. package/src/keywords/dependencies.ts +37 -17
  51. package/src/keywords/dependentRequired.ts +56 -38
  52. package/src/keywords/dependentSchemas.ts +37 -13
  53. package/src/keywords/deprecated.ts +32 -8
  54. package/src/keywords/enum.ts +30 -8
  55. package/src/keywords/exclusiveMaximum.ts +21 -2
  56. package/src/keywords/exclusiveMinimum.ts +22 -3
  57. package/src/keywords/format.ts +21 -2
  58. package/src/keywords/ifthenelse.ts +49 -5
  59. package/src/keywords/items.ts +27 -13
  60. package/src/keywords/maxItems.ts +22 -2
  61. package/src/keywords/maxLength.ts +30 -9
  62. package/src/keywords/maxProperties.ts +30 -9
  63. package/src/keywords/maximum.ts +28 -8
  64. package/src/keywords/minItems.ts +30 -9
  65. package/src/keywords/minLength.ts +30 -9
  66. package/src/keywords/minProperties.ts +26 -5
  67. package/src/keywords/minimum.ts +32 -13
  68. package/src/keywords/multipleOf.ts +33 -12
  69. package/src/keywords/not.ts +23 -10
  70. package/src/keywords/oneOf.ts +29 -9
  71. package/src/keywords/pattern.ts +35 -9
  72. package/src/keywords/properties.ts +34 -11
  73. package/src/keywords/propertyDependencies.test.ts +180 -0
  74. package/src/keywords/propertyDependencies.ts +173 -0
  75. package/src/keywords/propertyNames.ts +26 -14
  76. package/src/keywords/required.ts +31 -8
  77. package/src/keywords/type.ts +53 -16
  78. package/src/keywords/unevaluatedItems.ts +24 -8
  79. package/src/keywords/unevaluatedProperties.ts +24 -7
  80. package/src/keywords/uniqueItems.ts +23 -4
  81. package/src/mergeNode.ts +9 -4
  82. package/src/methods/getData.ts +1 -1
  83. package/src/settings.ts +2 -1
  84. package/src/types.ts +1 -1
  85. package/src/utils/isListOfStrings.ts +3 -0
  86. package/src/validate.test.ts +0 -2
  87. package/src/validateNode.ts +6 -3
  88. package/src/validateSchema.test.ts +312 -0
  89. package/tsconfig.json +11 -4
  90. package/tsconfig.test.json +9 -2
  91. package/tsdown.config.ts +1 -1
  92. package/Dockerfile +0 -6
  93. package/bowtie_jlib.js +0 -140
  94. package/remotes/draft04.json +0 -150
  95. package/remotes/draft06.json +0 -155
  96. package/remotes/draft07.json +0 -155
  97. package/remotes/draft2019-09.json +0 -42
  98. package/remotes/draft2019-09_meta_applicator.json +0 -53
  99. package/remotes/draft2019-09_meta_content.json +0 -14
  100. package/remotes/draft2019-09_meta_core.json +0 -54
  101. package/remotes/draft2019-09_meta_format.json +0 -11
  102. package/remotes/draft2019-09_meta_meta-data.json +0 -34
  103. package/remotes/draft2019-09_meta_validation.json +0 -95
  104. package/remotes/draft2020-12.json +0 -55
  105. package/remotes/draft2020-12_meta_applicator.json +0 -45
  106. package/remotes/draft2020-12_meta_content.json +0 -14
  107. package/remotes/draft2020-12_meta_core.json +0 -48
  108. package/remotes/draft2020-12_meta_format_annotation.json +0 -11
  109. package/remotes/draft2020-12_meta_format_assertion.json +0 -11
  110. package/remotes/draft2020-12_meta_meta_data.json +0 -34
  111. package/remotes/draft2020-12_meta_unevaluated.json +0 -12
  112. package/remotes/draft2020-12_meta_validation.json +0 -87
  113. package/remotes/index.ts +0 -48
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,10 @@ 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;
97
+ /** [SHARED USING ADD REMOTE] throw error on validation when ref cannot be resolved */
98
+ throwOnInvalidRef?: boolean;
94
99
  };
95
100
 
96
101
  export interface SchemaNode extends SchemaNodeMethodsType {
@@ -129,6 +134,7 @@ export interface SchemaNode extends SchemaNodeMethodsType {
129
134
  reducers: JsonSchemaReducer[];
130
135
  resolvers: JsonSchemaResolver[];
131
136
  validators: JsonSchemaValidator[];
137
+ schemaValidation?: ValidationAnnotation[];
132
138
 
133
139
  // parsed schema properties (registered by parsers)
134
140
  $id?: string;
@@ -140,7 +146,9 @@ export interface SchemaNode extends SchemaNodeMethodsType {
140
146
  contains?: SchemaNode;
141
147
  dependentRequired?: Record<string, string[]>;
142
148
  dependentSchemas?: Record<string, SchemaNode | boolean>;
149
+ deprecated?: boolean;
143
150
  else?: SchemaNode;
151
+ enum?: unknown[];
144
152
  if?: SchemaNode;
145
153
  /**
146
154
  * # Items-array schema - for all drafts
@@ -177,21 +185,40 @@ export interface SchemaNode extends SchemaNodeMethodsType {
177
185
  * | [AdditionalItems Specification](https://json-schema.org/draft/2019-09/draft-handrews-json-schema-02#additionalItems)
178
186
  */
179
187
  items?: SchemaNode;
188
+ maximum?: number;
189
+ minimum?: number;
190
+ maxItems?: number;
191
+ maxLength?: number;
192
+ maxProperties?: number;
193
+ minItems?: number;
194
+ minLength?: number;
195
+ minProperties?: number;
180
196
  not?: SchemaNode;
181
197
  oneOf?: SchemaNode[];
198
+ multipleOf?: number;
199
+ pattern?: RegExp;
182
200
  patternProperties?: { name: string; pattern: RegExp; node: SchemaNode }[];
201
+ propertyDependencies?: Record<string, Record<string, SchemaNode>>;
183
202
  properties?: Record<string, SchemaNode>;
184
203
  propertyNames?: SchemaNode;
204
+ required?: string[];
185
205
  then?: SchemaNode;
206
+ type?: string | string[];
186
207
  unevaluatedItems?: SchemaNode;
187
208
  unevaluatedProperties?: SchemaNode;
209
+ uniqueItems?: true;
188
210
  }
189
211
 
190
212
  /**
191
213
  * Fixed SchemaNode mixin methods
192
214
  */
193
215
  interface SchemaNodeMethodsType {
194
- compileSchema(schema: JsonSchema | BooleanSchema, evaluationPath?: string, schemaLocation?: string, dynamicId?: string): SchemaNode;
216
+ compileSchema(
217
+ schema: JsonSchema | BooleanSchema,
218
+ evaluationPath?: string,
219
+ schemaLocation?: string,
220
+ dynamicId?: string
221
+ ): SchemaNode;
195
222
  createError<T extends string = DefaultErrors>(code: T, data: AnnotationData, message?: string): JsonError;
196
223
  createAnnotation<T extends string = DefaultErrors>(code: T, data: AnnotationData, message?: string): JsonAnnotation;
197
224
  createSchema(data?: unknown): JsonSchema;
@@ -325,7 +352,20 @@ export const SchemaNodeMethods = {
325
352
  ...SchemaNodeMethods
326
353
  };
327
354
 
328
- addKeywords(node);
355
+ if (!isJsonSchema(schema) && !isBooleanSchema(schema)) {
356
+ node.schemaValidation = [
357
+ node.createError("schema-error", {
358
+ pointer: schemaLocation ?? evaluationPath,
359
+ schema,
360
+ value: undefined,
361
+ message: `JSON schema must be object or boolean - reveived: '${schema}'`
362
+ })
363
+ ];
364
+ return node;
365
+ }
366
+ const schemaValidation = addKeywords(node).filter((err) => err != null);
367
+ node.schemaValidation = sanitizeErrors(schemaValidation);
368
+
329
369
  return node;
330
370
  },
331
371
 
@@ -474,6 +514,13 @@ export const SchemaNodeMethods = {
474
514
  const errorsAsync: Promise<Maybe<ValidationAnnotation>[]>[] = [];
475
515
  sanitizeErrors(Array.isArray(errors) ? errors : [errors]).forEach((error) => {
476
516
  if (isJsonError(error)) {
517
+ if (node.context.throwOnInvalidRef && error.code === "ref-error") {
518
+ const refError = new Error("Invalid $ref: " + error.message);
519
+ // @ts-expect-error unknown error-property
520
+ refError.data = syncErrors;
521
+ throw refError;
522
+ }
523
+
477
524
  syncErrors.push(error);
478
525
  } else if (error instanceof Promise) {
479
526
  errorsAsync.push(error.then(sanitizeErrors));
@@ -497,15 +544,15 @@ export const SchemaNodeMethods = {
497
544
  * @returns the current node (not the remote schema-node)
498
545
  */
499
546
  addRemoteSchema(url: string, schema: JsonSchema | BooleanSchema): SchemaNode {
500
- // @draft >= 6
501
- if (isJsonSchema(schema)) {
502
- schema.$id = resolveUri(schema.$id || url);
503
- }
504
-
547
+ // @draft >= 6
548
+ if (isJsonSchema(schema)) {
549
+ schema.$id = resolveUri(schema.$id || url);
550
+ }
551
+
505
552
  const node = this as SchemaNode;
506
553
  const { context } = node;
507
554
  const schemaId = isJsonSchema(schema) ? schema.$schema : undefined;
508
- const draft = getDraft(context.drafts, schemaId ?? context.rootNode.schema?.$schema);
555
+ const draft = getDraft(context.drafts, schemaId ?? context.rootNode.schema?.$schema);
509
556
 
510
557
  const remoteNode: SchemaNode = {
511
558
  evaluationPath: "#",
@@ -565,20 +612,40 @@ const noRefMergeDrafts = ["draft-04", "draft-06", "draft-07"];
565
612
  export function addKeywords(node: SchemaNode) {
566
613
  if (node.schema.$ref && noRefMergeDrafts.includes(node.context.version)) {
567
614
  // for these draft versions only ref is validated
568
- node.context.keywords
615
+ return node.context.keywords
569
616
  .filter(({ keyword }) => whitelist.includes(keyword))
570
- .forEach((keyword) => execKeyword(keyword, node));
571
- return;
617
+ .map((keyword) => execKeyword(keyword, node));
572
618
  }
573
619
  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));
620
+ const errors = node.context.keywords
621
+ .filter(({ keyword }) => whitelist.includes(keyword) || keys.includes(keyword))
622
+ .map((keyword) => execKeyword(keyword, node));
623
+
624
+ // find unused keywords
625
+ if (node.context.withSchemaAnnotations) {
626
+ Object.keys(node.schema)
627
+ .filter(
628
+ (key) =>
629
+ !key.startsWith("x-") && node.context.keywords.find((keyword) => keyword.keyword === key) == null
630
+ )
631
+ .forEach((keyword) => {
632
+ errors.push(
633
+ node.createAnnotation("unknown-keyword-warning", {
634
+ pointer: `${node.schemaLocation}/${keyword}`,
635
+ schema: node.schema,
636
+ value: keyword,
637
+ draft: node.getDraftVersion()
638
+ })
639
+ );
640
+ });
641
+ }
642
+
643
+ return errors;
577
644
  }
578
645
 
579
646
  export function execKeyword(keyword: Keyword, node: SchemaNode) {
580
647
  // @todo consider first parsing all nodes
581
- keyword.parse?.(node);
648
+ const errors = keyword.parse?.(node);
582
649
  if (keyword.reduce && keyword.addReduce?.(node)) {
583
650
  node.reducers.push(keyword.reduce);
584
651
  }
@@ -588,4 +655,5 @@ export function execKeyword(keyword: Keyword, node: SchemaNode) {
588
655
  if (keyword.validate && keyword.addValidate?.(node)) {
589
656
  node.validators.push(keyword.validate);
590
657
  }
658
+ return errors;
591
659
  }
@@ -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,12 @@ 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 Error 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;
36
+ /** set to true to throw an Error when encountering an unresolvable ref */
37
+ throwOnInvalidRef?: boolean;
21
38
  };
22
39
 
23
40
  const defaultDrafts: Draft[] = [draft04, draft06, draft07, draft2019, draft2020];
@@ -35,8 +52,7 @@ export function compileSchema(schema: JsonSchema | BooleanSchema, options: Compi
35
52
  let formatAssertion = options.formatAssertion ?? true;
36
53
  const drafts = options.drafts ?? defaultDrafts;
37
54
  const draft = getDraft(drafts, isJsonSchema(schema) ? schema.$schema : undefined);
38
-
39
- const node: SchemaNode = {
55
+ const node: SchemaNode & { schemaErrors?: JsonError[]; schemaAnnotations: JsonAnnotation[] } = {
40
56
  evaluationPath: "#",
41
57
  lastIdPointer: "#",
42
58
  schemaLocation: "#",
@@ -54,6 +70,8 @@ export function compileSchema(schema: JsonSchema | BooleanSchema, options: Compi
54
70
  refs: {},
55
71
  ...copy(pick(draft, "methods", "keywords", "version", "formats", "errors")),
56
72
  getDataDefaultOptions: options.getDataDefaultOptions,
73
+ withSchemaAnnotations: options.withSchemaAnnotations ?? false,
74
+ throwOnInvalidRef: options.throwOnInvalidRef ?? false,
57
75
  drafts
58
76
  },
59
77
  ...SchemaNodeMethods
@@ -82,6 +100,40 @@ export function compileSchema(schema: JsonSchema | BooleanSchema, options: Compi
82
100
  node.context.keywords = node.context.keywords.filter((f) => f.keyword !== "format");
83
101
  }
84
102
 
85
- addKeywords(node);
103
+ if (!isJsonSchema(schema) && !isBooleanSchema(schema)) {
104
+ node.schemaErrors = [
105
+ node.createError("schema-error", {
106
+ pointer: "#",
107
+ schema,
108
+ value: undefined,
109
+ message: `JSON schema must be object or boolean - reveived: '${schema}'`
110
+ })
111
+ ];
112
+ return node;
113
+ }
114
+
115
+ // parse and validate schema
116
+ let schemaValidation = addKeywords(node).filter((err) => err != null);
117
+ schemaValidation = sanitizeErrors(schemaValidation);
118
+ const schemaErrors: JsonError[] = [];
119
+ const schemaAnnotations: JsonAnnotation[] = [];
120
+ schemaValidation.forEach((error) => {
121
+ if (isJsonError(error)) {
122
+ schemaErrors.push(error);
123
+ } else if (isJsonAnnotation(error)) {
124
+ schemaAnnotations.push(error);
125
+ }
126
+ });
127
+
128
+ if (options.throwOnInvalidSchema && schemaErrors.length > 0) {
129
+ const error = new Error("Invalid schema passed to compileSchema");
130
+ // @ts-expect-error unknown error-property
131
+ error.data = schemaErrors;
132
+ throw error;
133
+ }
134
+
135
+ node.schemaErrors = schemaErrors;
136
+ node.schemaAnnotations = schemaAnnotations;
137
+
86
138
  return node;
87
139
  }
@@ -4,7 +4,7 @@ import { isObject } from "../../utils/isObject";
4
4
  import { omit } from "../../utils/omit";
5
5
  import splitRef from "../../utils/splitRef";
6
6
  import { $refKeyword as draft06Keyword } from "../../draft06/keywords/$ref";
7
- import { SchemaNode } from "../../types";
7
+ import { isSchemaNode, JsonError, SchemaNode } from "../../types";
8
8
 
9
9
  export const $refKeyword: Keyword = {
10
10
  id: "$ref",
@@ -63,14 +63,9 @@ function resolveRef(this: SchemaNode, { pointer, path }: { pointer?: string; pat
63
63
  return this;
64
64
  }
65
65
  const resolvedNode = getRef(this);
66
- // console.log("RESOLVE REF", node.schema, "resolved ref", node.$ref, "=>", resolvedNode.schema);
67
- if (resolvedNode != null) {
66
+ if (isSchemaNode(resolvedNode)) {
68
67
  path?.push({ pointer: pointer!, node: resolvedNode });
69
- // console.log("resolve ref", node.$ref, "=>", resolvedNode.schema, Object.keys(node.context.refs));
70
- } else {
71
- console.log("failed resolving", this.$ref, "from", Object.keys(this.context.refs));
72
68
  }
73
-
74
69
  return resolvedNode;
75
70
  }
76
71
 
@@ -81,7 +76,7 @@ function compileNext(referencedNode: SchemaNode, evaluationPath = referencedNode
81
76
  return referencedNode.compileSchema(referencedSchema, `${evaluationPath}/$ref`, referencedSchema.schemaLocation);
82
77
  }
83
78
 
84
- function getRef(node: SchemaNode, $ref = node?.$ref): SchemaNode | undefined {
79
+ function getRef(node: SchemaNode, $ref = node?.$ref): SchemaNode | JsonError | undefined {
85
80
  if ($ref == null) {
86
81
  return node;
87
82
  }
@@ -100,8 +95,12 @@ function getRef(node: SchemaNode, $ref = node?.$ref): SchemaNode | undefined {
100
95
  // check for remote-host + pointer pair to switch rootSchema
101
96
  const fragments = splitRef($ref);
102
97
  if (fragments.length === 0) {
103
- // console.error("REF: INVALID", $ref);
104
- return undefined;
98
+ return node.createError("ref-error", {
99
+ ref: $ref,
100
+ pointer: node.evaluationPath,
101
+ schema: node.schema,
102
+ value: undefined
103
+ });
105
104
  }
106
105
 
107
106
  // resolve $ref as remote-host
@@ -111,8 +110,12 @@ function getRef(node: SchemaNode, $ref = node?.$ref): SchemaNode | undefined {
111
110
  if (node.context.remotes[$ref]) {
112
111
  return compileNext(node.context.remotes[$ref], node.evaluationPath);
113
112
  }
114
- // console.error("REF: UNFOUND 1", $ref, Object.keys(node.context.remotes));
115
- return undefined;
113
+ return node.createError("ref-error", {
114
+ ref: $ref,
115
+ pointer: node.evaluationPath,
116
+ schema: node.schema,
117
+ value: undefined
118
+ });
116
119
  }
117
120
 
118
121
  if (fragments.length === 2) {
@@ -142,9 +145,14 @@ function getRef(node: SchemaNode, $ref = node?.$ref): SchemaNode | undefined {
142
145
  }
143
146
  }
144
147
 
145
- // console.error("REF: UNFOUND 2", $ref);
148
+ // @todo returning error here breaks specs
146
149
  return undefined;
147
150
  }
148
151
 
149
- console.error("REF: INVALID", $ref);
152
+ return node.createError("ref-error", {
153
+ ref: $ref,
154
+ pointer: node.evaluationPath,
155
+ schema: node.schema,
156
+ value: undefined
157
+ });
150
158
  }
@@ -1,12 +1,26 @@
1
1
  import { Keyword, JsonSchemaValidatorParams } from "../../Keyword";
2
+ import { SchemaNode } from "../../SchemaNode";
2
3
 
3
4
  export const exclusiveMaximumKeyword: Keyword = {
4
5
  id: "exclusiveMaximum",
5
6
  keyword: "exclusiveMaximum",
7
+ parse,
6
8
  addValidate: ({ schema }) => schema.exclusiveMaximum === true || !isNaN(schema.maximum),
7
9
  validate: validateExclusiveMaximum
8
10
  };
9
11
 
12
+ function parse(node: SchemaNode) {
13
+ const { exclusiveMaximum } = node.schema;
14
+ if (exclusiveMaximum != null && !(typeof exclusiveMaximum === "number" || typeof exclusiveMaximum === "boolean")) {
15
+ return node.createError("schema-error", {
16
+ pointer: node.evaluationPath,
17
+ schema: node.schema,
18
+ value: undefined,
19
+ message: `Keyword 'exclusiveMaximum' must be a number - received '${typeof exclusiveMaximum}'`
20
+ });
21
+ }
22
+ }
23
+
10
24
  function validateExclusiveMaximum({ node, data, pointer }: JsonSchemaValidatorParams) {
11
25
  if (typeof data !== "number") {
12
26
  return undefined;
@@ -1,12 +1,26 @@
1
1
  import { Keyword, JsonSchemaValidatorParams } from "../../Keyword";
2
+ import { SchemaNode } from "../../SchemaNode";
2
3
 
3
4
  export const exclusiveMinimumKeyword: Keyword = {
4
5
  id: "exclusiveMinimum",
5
6
  keyword: "exclusiveMinimum",
7
+ parse,
6
8
  addValidate: ({ schema }) => schema.exclusiveMinimum === true || !isNaN(schema.minimum),
7
9
  validate: validateExclusiveMinimum
8
10
  };
9
11
 
12
+ function parse(node: SchemaNode) {
13
+ const { exclusiveMinimum } = node.schema;
14
+ if (exclusiveMinimum != null && !(typeof exclusiveMinimum === "number" || typeof exclusiveMinimum === "boolean")) {
15
+ return node.createError("schema-error", {
16
+ pointer: node.evaluationPath,
17
+ schema: node.schema,
18
+ value: undefined,
19
+ message: `Keyword 'exclusiveMinimum' must be a number - received '${typeof exclusiveMinimum}'`
20
+ });
21
+ }
22
+ }
23
+
10
24
  function validateExclusiveMinimum({ node, data, pointer }: JsonSchemaValidatorParams) {
11
25
  if (typeof data !== "number") {
12
26
  return undefined;
@@ -0,0 +1,20 @@
1
+ import { compileSchema as _compileSchema, type CompileOptions } from "../compileSchema";
2
+ import { strict as assert } from "assert";
3
+ import { draft04 } from "../draft04";
4
+ import { JsonSchema } from "../types";
5
+
6
+ const drafts = [draft04];
7
+ function compileSchema(schema: JsonSchema, options: CompileOptions = {}) {
8
+ return _compileSchema(schema, { ...options, drafts });
9
+ }
10
+
11
+ describe("validateSchema (4)", () => {
12
+ it("should error if `exclusiveMinimum` is not a number or boolean", () => {
13
+ const { schemaErrors } = compileSchema({ exclusiveMinimum: [] });
14
+ assert.equal(schemaErrors?.length, 1);
15
+ });
16
+ it("should error if `exclusiveMaximum` is not a number or boolean", () => {
17
+ const { schemaErrors } = compileSchema({ exclusiveMaximum: [] });
18
+ assert.equal(schemaErrors?.length, 1);
19
+ });
20
+ });
@@ -1,6 +1,6 @@
1
1
  import { Keyword, JsonSchemaValidatorParams, ValidationPath } from "../../Keyword";
2
2
  import { resolveRef } from "../../keywords/$ref";
3
- import { SchemaNode } from "../../types";
3
+ import { isSchemaNode, SchemaNode } from "../../types";
4
4
  import { resolveUri } from "../../utils/resolveUri";
5
5
  import { validateNode } from "../../validateNode";
6
6
 
@@ -51,16 +51,26 @@ function parseRef(node: SchemaNode) {
51
51
 
52
52
  function validateRef({ node, data, pointer = "#", path }: JsonSchemaValidatorParams) {
53
53
  const nextNode = resolveAllRefs(node, pointer, path);
54
- if (nextNode == null) {
55
- return undefined;
54
+ if (!isSchemaNode(nextNode)) {
55
+ return node.createError("ref-error", {
56
+ ref: node.schema.$ref,
57
+ pointer,
58
+ schema: node.schema,
59
+ value: data
60
+ });
56
61
  }
57
62
  return validateNode(nextNode, data, pointer, path);
58
63
  }
59
64
 
60
65
  function resolveAllRefs(node: SchemaNode, pointer: string, path: ValidationPath) {
61
66
  const nextNode = node.resolveRef({ pointer, path });
62
- if (nextNode == null) {
63
- return undefined;
67
+ if (!isSchemaNode(nextNode)) {
68
+ return node.createError("ref-error", {
69
+ ref: node.schema.$ref,
70
+ pointer,
71
+ schema: node.schema,
72
+ value: undefined
73
+ });
64
74
  }
65
75
  if (nextNode !== node && nextNode) {
66
76
  return resolveAllRefs(nextNode, pointer, path);
@@ -1,5 +1,6 @@
1
1
  import { strict as assert } from "assert";
2
2
  import { compileSchema } from "../../compileSchema";
3
+ import { isJsonError } from "../../types";
3
4
 
4
5
  describe("keyword : $ref : resolve", () => {
5
6
  it("should return undefined for missing reference", () => {
@@ -8,7 +9,8 @@ describe("keyword : $ref : resolve", () => {
8
9
  minLength: 1
9
10
  }).resolveRef();
10
11
 
11
- assert.deepEqual(node, undefined);
12
+ assert(isJsonError(node));
13
+ assert.deepEqual(node.code, "ref-error");
12
14
  });
13
15
 
14
16
  it("should resolve $ref from definitions", () => {
@@ -4,7 +4,7 @@ import splitRef from "../../utils/splitRef";
4
4
  import { omit } from "../../utils/omit";
5
5
  import { isObject } from "../../utils/isObject";
6
6
  import { validateNode } from "../../validateNode";
7
- import { SchemaNode } from "../../types";
7
+ import { isSchemaNode, JsonError, SchemaNode } from "../../types";
8
8
  import { get, split } from "@sagold/json-pointer";
9
9
  import { reduceRef } from "../../keywords/$ref";
10
10
 
@@ -73,7 +73,9 @@ export function parseRef(node: SchemaNode) {
73
73
  export function resolveRef(this: SchemaNode, { pointer, path }: { pointer?: string; path?: ValidationPath } = {}) {
74
74
  if (this.schema.$recursiveRef) {
75
75
  const nextNode = resolveRecursiveRef(this, path ?? []);
76
- path?.push({ pointer: pointer!, node: nextNode! });
76
+ if (isSchemaNode(nextNode)) {
77
+ path?.push({ pointer: pointer!, node: nextNode! });
78
+ }
77
79
  return nextNode;
78
80
  }
79
81
 
@@ -82,10 +84,8 @@ export function resolveRef(this: SchemaNode, { pointer, path }: { pointer?: stri
82
84
  }
83
85
 
84
86
  const resolvedNode = getRef(this);
85
- if (resolvedNode != null) {
87
+ if (isSchemaNode(resolvedNode)) {
86
88
  path?.push({ pointer: pointer!, node: resolvedNode });
87
- } else {
88
- // console.log("failed resolving", node.$ref, "from", Object.keys(node.context.refs));
89
89
  }
90
90
  return resolvedNode;
91
91
  }
@@ -93,13 +93,18 @@ export function resolveRef(this: SchemaNode, { pointer, path }: { pointer?: stri
93
93
  function validateRef({ node, data, pointer = "#", path }: JsonSchemaValidatorParams) {
94
94
  const nextNode = node.resolveRef({ pointer, path });
95
95
  if (nextNode != null) {
96
- // recursively resolveRef and validate
97
96
  return validateNode(nextNode, data, pointer, path);
98
97
  }
98
+ return node.createError("ref-error", {
99
+ ref: node.schema.$ref ?? node.schema.$recursiveRef,
100
+ pointer,
101
+ schema: node.schema,
102
+ value: data
103
+ });
99
104
  }
100
105
 
101
106
  // 1. https://json-schema.org/draft/2019-09/json-schema-core#scopes
102
- function resolveRecursiveRef(node: SchemaNode, path: ValidationPath): SchemaNode | undefined {
107
+ function resolveRecursiveRef(node: SchemaNode, path: ValidationPath): SchemaNode | JsonError {
103
108
  const history = path;
104
109
 
105
110
  // RESTRICT BY CHANGE IN BASE-URL
@@ -135,7 +140,7 @@ function compileNext(referencedNode: SchemaNode, evaluationPath = referencedNode
135
140
  return referencedNode.compileSchema(referencedSchema, `${evaluationPath}/$ref`, referencedNode.schemaLocation);
136
141
  }
137
142
 
138
- export default function getRef(node: SchemaNode, $ref = node?.$ref): SchemaNode | undefined {
143
+ export default function getRef(node: SchemaNode, $ref = node?.$ref): SchemaNode | JsonError {
139
144
  if ($ref == null) {
140
145
  return node;
141
146
  }
@@ -153,7 +158,12 @@ export default function getRef(node: SchemaNode, $ref = node?.$ref): SchemaNode
153
158
  const fragments = splitRef($ref);
154
159
  if (fragments.length === 0) {
155
160
  // console.error("REF: INVALID", $ref);
156
- return undefined;
161
+ return node.createError("ref-error", {
162
+ ref: $ref,
163
+ pointer: node.evaluationPath,
164
+ schema: node.schema,
165
+ value: undefined
166
+ });
157
167
  }
158
168
 
159
169
  // resolve $ref as remote-host
@@ -175,7 +185,12 @@ export default function getRef(node: SchemaNode, $ref = node?.$ref): SchemaNode
175
185
  }
176
186
  }
177
187
  // console.error("REF: UNFOUND 1", $ref);
178
- return undefined;
188
+ return node.createError("ref-error", {
189
+ ref: $ref,
190
+ pointer: node.evaluationPath,
191
+ schema: node.schema,
192
+ value: undefined
193
+ });
179
194
  }
180
195
 
181
196
  if (fragments.length === 2) {
@@ -206,16 +221,25 @@ export default function getRef(node: SchemaNode, $ref = node?.$ref): SchemaNode
206
221
  // @ts-expect-error random path
207
222
  currentNode = currentNode[property];
208
223
  if (currentNode == null) {
209
- console.error("REF: FAILED RESOLVING ref json-pointer", fragments[1]);
210
- return undefined;
224
+ // console.error("REF: FAILED RESOLVING ref json-pointer", fragments[1]);
225
+ return node.createError("ref-error", {
226
+ ref: $ref,
227
+ pointer: node.evaluationPath,
228
+ schema: node.schema,
229
+ value: undefined,
230
+ host: fragments[0],
231
+ local: fragments[1]
232
+ });
211
233
  }
212
234
  }
213
235
  return currentNode;
214
236
  }
215
-
216
- // console.error("REF: UNFOUND 2", $ref);
217
- return undefined;
218
237
  }
219
238
 
220
- console.error("REF: UNHANDLED", $ref);
239
+ return node.createError("ref-error", {
240
+ ref: $ref,
241
+ pointer: node.evaluationPath,
242
+ schema: node.schema,
243
+ value: undefined
244
+ });
221
245
  }