json-schema-library 11.4.1 → 11.5.1

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.4.1",
3
+ "version": "11.5.1",
4
4
  "description": "Customizable and hackable json-validator and json-schema utilities for traversal, data generation and validation",
5
5
  "types": "./dist/index.d.cts",
6
6
  "exports": {
package/src/SchemaNode.ts CHANGED
@@ -41,7 +41,7 @@ import { getNode } from "./getNode";
41
41
  import { getNodeChild } from "./getNodeChild";
42
42
  import { DataNode } from "./methods/toDataNodes";
43
43
 
44
- const { DYNAMIC_PROPERTIES, REGEX_FLAGS, DECLARATOR_ONEOF } = settings;
44
+ const { DYNAMIC_PROPERTIES, REGEX_FLAGS, DECLARATOR_ONEOF, VALID_ANNOTATION_KEYWORDS } = settings;
45
45
 
46
46
  export function isSchemaNode(value: unknown): value is SchemaNode {
47
47
  return isObject(value) && Array.isArray(value?.reducers) && Array.isArray(value?.resolvers);
@@ -624,7 +624,10 @@ export function addKeywords(node: SchemaNode) {
624
624
  .map((keyword) => execKeyword(keyword, node));
625
625
 
626
626
  keys.filter(
627
- (key) => !key.startsWith("x-") && node.context.keywords.find((keyword) => keyword.keyword === key) == null
627
+ (key) =>
628
+ !key.startsWith("x-") &&
629
+ !VALID_ANNOTATION_KEYWORDS.includes(key) &&
630
+ node.context.keywords.find((keyword) => keyword.keyword === key) == null
628
631
  ).forEach((keyword) => {
629
632
  errors.push(
630
633
  node.createAnnotation("unknown-keyword-warning", {
@@ -84,6 +84,30 @@ describe("compileSchema draft-version", () => {
84
84
  });
85
85
  });
86
86
 
87
+ describe("compileSchema remotes", () => {
88
+ it("should add list of remotes to returned SchemaNode node", () => {
89
+ const node = compileSchema(
90
+ {
91
+ type: "object",
92
+ required: ["string", "number"],
93
+ properties: {
94
+ string: { $ref: "https://remote-a.com/schema" },
95
+ number: { $ref: "https://remote-b.com/schema" }
96
+ }
97
+ },
98
+ {
99
+ drafts: [draft2020],
100
+ remotes: [
101
+ { $id: "https://remote-a.com/schema", type: "string", default: "a" },
102
+ { $id: "https://remote-b.com/schema", type: "number", default: 9 }
103
+ ]
104
+ }
105
+ );
106
+ const data = node.getData();
107
+ assert.deepEqual(data, { string: "a", number: 9 });
108
+ });
109
+ });
110
+
87
111
  describe("compileSchema vocabulary", () => {
88
112
  it("should add remote schema on compile", () => {
89
113
  const remote = compileSchema({
@@ -67,6 +67,10 @@ export type CompileOptions = {
67
67
  * Set node and its remote schemata as remote schemata for this node and schema to resolve $ref
68
68
  */
69
69
  remote?: SchemaNode;
70
+ /**
71
+ * a list of remotes to add, requires a unique $id for each schema. Will be ignored if `remote` is set
72
+ */
73
+ remotes?: JsonSchema[];
70
74
  /**
71
75
  * Enables `format`-keyword assertions when this is set tor `true` or sets assertion as defined by
72
76
  * the given meta-schema. Set to `false` to deactivate format validation.
@@ -94,6 +98,18 @@ function getDraft(drafts: Draft[], $schema: string) {
94
98
  * node will be reused for each task, but will create a compiledNode for bound data.
95
99
  */
96
100
  export function compileSchema(schema: JsonSchema | BooleanSchema, options: CompileOptions = {}) {
101
+ let remote = options.remote;
102
+ if (Array.isArray(options.remotes) && options.remotes.length > 0 && !options.remote) {
103
+ const remotes = [...options.remotes];
104
+ remotes.forEach((remote, index) => {
105
+ if (remote.$id == null) {
106
+ throw new Error(`required $id on remotes[${index}] is missing`);
107
+ }
108
+ });
109
+ remote = compileSchema(remotes.shift()!);
110
+ remotes.forEach((r) => remote?.addRemoteSchema(r.$id, r));
111
+ }
112
+
97
113
  let formatAssertion = options.formatAssertion ?? true;
98
114
  const drafts = options.drafts ?? defaultDrafts;
99
115
  const draft = getDraft(drafts, isJsonSchema(schema) ? (options.draft ?? schema.$schema) : undefined);
@@ -110,7 +126,7 @@ export function compileSchema(schema: JsonSchema | BooleanSchema, options: Compi
110
126
  context: {
111
127
  remotes: {},
112
128
  dynamicAnchors: {},
113
- ...(options.remote?.context ?? {}),
129
+ ...(remote?.context ?? {}),
114
130
  anchors: {},
115
131
  refs: {},
116
132
  ...copy(pick(draft, "methods", "keywords", "version", "formats", "errors")),
@@ -125,7 +141,7 @@ export function compileSchema(schema: JsonSchema | BooleanSchema, options: Compi
125
141
  node.context.rootNode = node;
126
142
  node.context.remotes[(isJsonSchema(schema) ? schema.$id : undefined) ?? "#"] = node;
127
143
 
128
- if (options.remote) {
144
+ if (remote) {
129
145
  const metaSchema = getRef(node, node.schema.$schema);
130
146
  if (isSchemaNode(metaSchema) && metaSchema.schema.$vocabulary) {
131
147
  const vocabs = Object.keys(metaSchema.schema.$vocabulary);
@@ -4,6 +4,7 @@ import { oneOfFuzzyKeyword } from "./keywords/oneOf";
4
4
  import { render } from "./errors/render";
5
5
 
6
6
  /**
7
+ * @deprecated
7
8
  * @draft-editor https://json-schema.org/draft/2020-12/release-notes
8
9
  *
9
10
  * Uses Draft 2020-12 and changes resolveOneOf to be fuzzy
@@ -1,4 +1,5 @@
1
1
  import { compileSchema } from "../compileSchema";
2
+ import { draft2020 } from "../draft2020";
2
3
  import { strict as assert } from "assert";
3
4
 
4
5
  describe("keyword : unevaluatedProperties : validation", () => {
@@ -13,4 +14,60 @@ describe("keyword : unevaluatedProperties : validation", () => {
13
14
  });
14
15
  assert.equal(errors.length, 0);
15
16
  });
17
+
18
+ it("should not return unevaluated-property-error for a property that fails format validation", () => {
19
+ const node = compileSchema(
20
+ {
21
+ type: "object",
22
+ properties: {
23
+ name: { type: "string" },
24
+ email: { type: "string", format: "email" }
25
+ },
26
+ unevaluatedProperties: false
27
+ },
28
+ { drafts: [draft2020] }
29
+ );
30
+
31
+ const { errors } = node.validate({ name: "Alice", email: "not-an-email" });
32
+
33
+ const unevaluatedErrors = errors.filter((e) => e.code === "unevaluated-property-error");
34
+ assert.equal(unevaluatedErrors.length, 0, "should not flag email as unevaluated");
35
+ });
36
+
37
+ it("should not return unevaluated-property-error for a property that fails type validation", () => {
38
+ const node = compileSchema(
39
+ {
40
+ type: "object",
41
+ properties: {
42
+ name: { type: "string" },
43
+ age: { type: "number" }
44
+ },
45
+ unevaluatedProperties: false
46
+ },
47
+ { drafts: [draft2020] }
48
+ );
49
+
50
+ const { errors } = node.validate({ name: "Alice", age: "not-a-number" });
51
+
52
+ const unevaluatedErrors = errors.filter((e) => e.code === "unevaluated-property-error");
53
+ assert.equal(unevaluatedErrors.length, 0, "should not flag age as unevaluated");
54
+ });
55
+
56
+ it("should still return unevaluated-property-error for truly unknown properties", () => {
57
+ const node = compileSchema(
58
+ {
59
+ type: "object",
60
+ properties: {
61
+ name: { type: "string" }
62
+ },
63
+ unevaluatedProperties: false
64
+ },
65
+ { drafts: [draft2020] }
66
+ );
67
+
68
+ const { errors } = node.validate({ name: "Alice", unknown: "value" });
69
+
70
+ const unevaluatedErrors = errors.filter((e) => e.code === "unevaluated-property-error");
71
+ assert.equal(unevaluatedErrors.length, 1, "should flag unknown as unevaluated");
72
+ });
16
73
  });
@@ -56,6 +56,13 @@ function validateUnevaluatedProperties({ node, data, pointer, path }: JsonSchema
56
56
 
57
57
  const errors: ValidationReturnType = [];
58
58
  for (const propertyName of unevaluated) {
59
+ // Properties defined directly on this schema object are always
60
+ // evaluated by the "properties" keyword, regardless of whether the
61
+ // value passes validation (per JSON Schema spec, annotations from
62
+ // adjacent keywords are always collected)
63
+ if (node.properties?.[propertyName]) {
64
+ continue;
65
+ }
59
66
  if (isPropertyEvaluated({ node, data, key: propertyName, pointer, path })) {
60
67
  continue;
61
68
  }
package/src/settings.ts CHANGED
@@ -18,6 +18,8 @@ export default {
18
18
  "propertyDependencies"
19
19
  ],
20
20
  REGEX_FLAGS: "u",
21
+ /** additional keywords that should not produce an unknown-keyword-warning */
22
+ VALID_ANNOTATION_KEYWORDS: ["$id", "$schema", "title", "description", "default", "oneOfProperty"],
21
23
  /**
22
24
  * properties to keep from a $ref-schema when resolving a $ref (recursively)
23
25
  * this allows to overwrite specified properties locally on a $ref-definition
@@ -43,5 +45,5 @@ export default {
43
45
  * type: "object"
44
46
  * }
45
47
  */
46
- PROPERTIES_TO_MERGE: ["title", "description", "options", "x-options", "readOnly", "writeOnly"]
48
+ PROPERTIES_TO_MERGE: ["title", "description", "default", "options", "x-options", "readOnly", "writeOnly"]
47
49
  };
@@ -263,7 +263,7 @@ describe("validateSchema", () => {
263
263
  assert.equal(schemaAnnotations.length, 1);
264
264
  assert.equal(schemaAnnotations[0].data.pointer, "#/properties/headline/options");
265
265
  });
266
- it("should return not unknown keywords starting with 'x-'", () => {
266
+ it("should not return unknown keywords starting with 'x-'", () => {
267
267
  const { schemaAnnotations } = compileSchema({
268
268
  properties: {
269
269
  headline: {
@@ -274,6 +274,15 @@ describe("validateSchema", () => {
274
274
  });
275
275
  assert.equal(schemaAnnotations.length, 0);
276
276
  });
277
+ it("should not return valid annotation keywords", () => {
278
+ const { schemaAnnotations } = compileSchema({
279
+ type: "string",
280
+ title: "input",
281
+ description: "any string",
282
+ default: "hey"
283
+ });
284
+ assert.equal(schemaAnnotations.length, 0);
285
+ });
277
286
  it("should return removed keywords from old drafts as annotation", () => {
278
287
  const { schemaAnnotations } = compileSchema({
279
288
  properties: {