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/CHANGELOG.md +6 -0
- package/README.md +2 -0
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -1
- package/dist/index.d.mts +7 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/jlib.js +2 -2
- package/package.json +1 -1
- package/src/SchemaNode.ts +5 -2
- package/src/compileSchema.test.ts +24 -0
- package/src/compileSchema.ts +18 -2
- package/src/draftEditor.ts +1 -0
- package/src/keywords/unevaluatedProperties.test.ts +57 -0
- package/src/keywords/unevaluatedProperties.ts +7 -0
- package/src/settings.ts +3 -1
- package/src/validateSchema.test.ts +10 -1
package/package.json
CHANGED
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) =>
|
|
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({
|
package/src/compileSchema.ts
CHANGED
|
@@ -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
|
-
...(
|
|
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 (
|
|
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);
|
package/src/draftEditor.ts
CHANGED
|
@@ -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
|
|
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: {
|