json-schema-library
Version:
Customizable and hackable json-validator and json-schema utilities for traversal, data generation and validation
538 lines (500 loc) • 22.2 kB
text/typescript
import { strict as assert } from "assert";
import { compileSchema } from "../compileSchema";
import { isJsonError, isSchemaNode } from "../types";
import { reduceOneOfDeclarator, reduceOneOfFuzzy } from "./oneOf";
import settings from "../settings";
import { draftEditor } from "../draftEditor";
const DECLARATOR_ONEOF = settings.DECLARATOR_ONEOF;
describe("keyword : oneof : validate", () => {
it("should validate matching oneOf", () => {
const { errors } = compileSchema({
oneOf: [
{ type: "object", properties: { value: { type: "string" } } },
{ type: "object", properties: { value: { type: "integer" } } }
]
}).validate({ value: "a string" });
assert.equal(errors.length, 0);
});
it("should return error for non-matching oneOf", () => {
const { errors } = compileSchema({
type: "object",
oneOf: [
{ type: "object", properties: { value: { type: "string" } } },
{ type: "object", properties: { value: { type: "integer" } } }
]
}).validate({ value: [] });
assert.equal(errors.length, 1);
assert.equal(errors[0].code, "one-of-error");
});
});
describe("keyword : oneOf : reduce", () => {
it("should resolve matching value schema", () => {
const { node } = compileSchema({
oneOf: [
{ type: "string", title: "A String" },
{ type: "number", title: "A Number" }
]
}).reduceNode(111);
assert.deepEqual(node?.schema, { type: "number", title: "A Number" });
assert.equal(node.oneOfIndex, 1, "should have exposed correct resolved oneOfIndex");
});
it("should return error if no matching schema could be found", () => {
const { node, error } = compileSchema({
oneOf: [
{ type: "string", title: "A String" },
{ type: "number", title: "A Number" }
]
}).reduceNode(true);
assert(isJsonError(error));
assert.equal(node, undefined);
});
it("should return error if multiple schema match", () => {
const { node, error } = compileSchema({
oneOf: [
{ type: "string", minLength: 1 },
{ type: "string", maxLength: 3 }
]
}).reduceNode("12");
assert(isJsonError(error));
assert.equal(node, undefined);
});
it("should reduce nested oneOf objects using ref", () => {
const { node } = compileSchema({
$defs: { withData: { oneOf: [{ required: ["b"], properties: { b: { type: "number" } } }] } },
oneOf: [{ required: ["a"], properties: { a: { type: "string" } } }, { $ref: "#/$defs/withData" }]
}).reduceNode({ b: 111 });
assert.deepEqual(node?.schema, { required: ["b"], properties: { b: { type: "number" } } });
// @note that we override nested oneOfIndex
assert.equal(node.oneOfIndex, 1, "should have exposed correct resolved oneOfIndex");
});
it("should reduce nested oneOf boolean schema using ref", () => {
const { node } = compileSchema({
$defs: { withData: { oneOf: [{ required: ["b"], properties: { b: true } }] } },
oneOf: [{ required: ["a"], properties: { a: false } }, { $ref: "#/$defs/withData" }]
}).reduceNode({ b: 111 });
assert.deepEqual(node?.schema, { required: ["b"], properties: { b: true } });
assert.equal(node.oneOfIndex, 1, "should have exposed correct resolved oneOfIndex");
});
it("should resolve matching object schema", () => {
const { node } = compileSchema({
oneOf: [
{
type: "object",
properties: { title: { type: "string" } }
},
{
type: "object",
properties: { title: { type: "number" } }
}
]
}).reduceNode({ title: 4 });
assert.deepEqual(node?.schema, { type: "object", properties: { title: { type: "number" } } });
assert.equal(node.oneOfIndex, 1, "should have exposed correct resolved oneOfIndex");
});
it("should return matching oneOf, for objects missing properties", () => {
const { node } = compileSchema({
oneOf: [
{
type: "object",
additionalProperties: { type: "string" }
},
{
type: "object",
additionalProperties: { type: "number" }
}
]
}).reduceNode({ title: 4, test: 2 });
assert.deepEqual(node?.schema, { type: "object", additionalProperties: { type: "number" } });
assert.equal(node.oneOfIndex, 1, "should have exposed correct resolved oneOfIndex");
});
});
describe("keyword : oneof-fuzzy : reduce", () => {
it("should return schema with matching type", () => {
const node = compileSchema({
oneOf: [{ type: "string" }, { type: "number" }, { type: "object" }]
});
const res = reduceOneOfFuzzy({ node, data: 4, pointer: "#", path: [] });
assert.deepEqual(res?.schema, { type: "number" });
assert.equal(res.oneOfIndex, 1, "should have exposed correct resolved oneOfIndex");
});
it("should return schema with matching pattern", () => {
const node = compileSchema({
oneOf: [
{ type: "string", pattern: "obelix" },
{ type: "string", pattern: "asterix" }
]
});
const res = reduceOneOfFuzzy({ node, data: "anasterixcame", pointer: "#", path: [] });
assert.deepEqual(res?.schema, { type: "string", pattern: "asterix" });
assert.equal(res.oneOfIndex, 1, "should have exposed correct resolved oneOfIndex");
});
it("should resolve $ref before schema", () => {
const node = compileSchema({
definitions: {
a: { type: "string", pattern: "obelix" },
b: { type: "string", pattern: "asterix" }
},
oneOf: [{ $ref: "#/definitions/a" }, { $ref: "#/definitions/b" }]
});
const res = reduceOneOfFuzzy({ node, data: "anasterixcame", pointer: "#", path: [] });
assert.deepEqual(res?.schema, { type: "string", pattern: "asterix" });
assert.equal(res.oneOfIndex, 1, "should have exposed correct resolved oneOfIndex");
});
describe("object", () => {
it("should return schema with matching properties", () => {
const node = compileSchema({
oneOf: [
{ type: "object", properties: { title: { type: "string" } } },
{ type: "object", properties: { description: { type: "string" } } },
{ type: "object", properties: { content: { type: "string" } } }
]
});
const res = reduceOneOfFuzzy({ node, data: { description: "..." }, pointer: "#", path: [] });
assert.deepEqual(res?.schema, {
type: "object",
properties: { description: { type: "string" } }
});
});
it("should return schema matching nested properties", () => {
const node = compileSchema({
oneOf: [
{ type: "object", properties: { title: { type: "number" } } },
{ type: "object", properties: { title: { type: "string" } } }
]
});
const res = reduceOneOfFuzzy({ node, data: { title: "asterix" }, pointer: "#", path: [] });
assert.deepEqual(res?.schema, {
type: "object",
properties: { title: { type: "string" } }
});
});
it("should return schema with least missing properties", () => {
const t = { type: "number" };
const node = compileSchema({
oneOf: [
{ type: "object", properties: { a: t, c: t, d: t } },
{ type: "object", properties: { a: t, b: t, c: t } },
{ type: "object", properties: { a: t, d: t, e: t } }
]
});
const res = reduceOneOfFuzzy({ node, data: { a: 0, b: 1 }, pointer: "#", path: [] });
assert.deepEqual(res?.schema, {
type: "object",
properties: { a: t, b: t, c: t }
});
assert.equal(res.oneOfIndex, 1, "should have exposed correct resolved oneOfIndex");
});
it("should only count properties that match the schema", () => {
const t = { type: "number" };
const node = compileSchema({
oneOf: [
{ type: "object", properties: { a: { type: "string" }, b: t, c: t } },
{ type: "object", properties: { a: { type: "boolean" }, b: t, d: t } },
{ type: "object", properties: { a: { type: "number" }, b: t, e: t } }
]
});
const res = reduceOneOfFuzzy({ node, data: { a: true, b: 1 }, pointer: "#", path: [] });
assert.deepEqual(res?.schema, {
type: "object",
properties: { a: { type: "boolean" }, b: t, d: t }
});
});
it("should find correct pay type", () => {
const node = compileSchema({
type: "object",
oneOf: [
{
type: "object",
properties: { type: { type: "string", default: "free", pattern: "^free" } }
},
{
type: "object",
properties: {
redirectUrl: { format: "url", type: "string" },
type: { type: "string", default: "teaser", pattern: "^teaser" }
}
},
{
type: "object",
properties: {
redirectUrl: { format: "url", type: "string" },
type: { type: "string", default: "article", pattern: "^article" }
}
}
]
});
const res = reduceOneOfFuzzy({
pointer: "#",
path: [],
node,
data: { type: "teaser", redirectUrl: "http://example.com/test/pay/article.html" }
});
assert.deepEqual(res?.schema, {
type: "object",
properties: {
redirectUrl: { format: "url", type: "string" },
type: { type: "string", default: "teaser", pattern: "^teaser" }
}
});
});
});
});
describe("keyword : oneof-property : reduce", () => {
describe("object", () => {
it("should return schema matching oneOfProperty", () => {
const node = compileSchema({
[DECLARATOR_ONEOF]: "name",
oneOf: [
{
type: "object",
properties: { name: { type: "string", pattern: "^1$" }, title: { type: "number" } }
},
{
type: "object",
properties: { name: { type: "string", pattern: "^2$" }, title: { type: "number" } }
},
{
type: "object",
properties: { name: { type: "string", pattern: "^3$" }, title: { type: "number" } }
}
]
});
const res = reduceOneOfDeclarator({ node, data: { name: "2", title: 123 }, pointer: "#", path: [] });
assert.deepEqual(res?.schema, {
type: "object",
properties: {
name: { type: "string", pattern: "^2$" },
title: { type: "number" }
}
});
assert.equal(res.oneOfIndex, 1, "should have exposed correct resolved oneOfIndex");
});
it("should return schema matching oneOfProperty even it is invalid", () => {
const node = compileSchema({
[DECLARATOR_ONEOF]: "name",
oneOf: [
{
type: "object",
properties: { name: { type: "string", pattern: "^1$" }, title: { type: "number" } }
},
{
type: "object",
properties: { name: { type: "string", pattern: "^2$" }, title: { type: "number" } }
},
{
type: "object",
properties: { name: { type: "string", pattern: "^3$" }, title: { type: "number" } }
}
]
});
const res = reduceOneOfDeclarator({
node,
data: { name: "2", title: "not a number" },
pointer: "#",
path: []
});
assert.deepEqual(res?.schema, {
type: "object",
properties: {
name: { type: "string", pattern: "^2$" },
title: { type: "number" }
}
});
});
it("should return an error if value at oneOfProperty is undefined", () => {
const node = compileSchema({
[DECLARATOR_ONEOF]: "name",
oneOf: [
{
type: "object",
properties: { name: { type: "string", pattern: "^1$" }, title: { type: "number" } }
},
{
type: "object",
properties: { name: { type: "string", pattern: "^2$" }, title: { type: "number" } }
},
{
type: "object",
properties: { name: { type: "string", pattern: "^3$" }, title: { type: "number" } }
}
]
});
const res = reduceOneOfDeclarator({ node, data: { title: "not a number" }, pointer: "#", path: [] });
assert(isJsonError(res), "expected result to be an error");
assert.deepEqual(res.code, "missing-one-of-property-error");
});
it("should return correct reference", () => {
const node = compileSchema({
[DECLARATOR_ONEOF]: "name",
oneOf: [{ $ref: "#/$defs/first" }, { $ref: "#/$defs/second" }],
$defs: {
first: {
properties: { name: { type: "string", const: "first" }, title: { type: "number" } }
},
second: {
properties: { name: { type: "string", const: "second" }, title: { type: "number" } }
}
}
});
const res = reduceOneOfDeclarator({ node, data: { name: "second" }, pointer: "#", path: [] });
assert(isSchemaNode(res), "expected result to be a node");
assert.deepEqual(res.schema, {
properties: { name: { type: "string", const: "second" }, title: { type: "number" } }
});
});
it("should return correct reference even if data is not fully valid", () => {
const node = compileSchema({
[DECLARATOR_ONEOF]: "name",
oneOf: [{ $ref: "#/$defs/first" }, { $ref: "#/$defs/second" }],
$defs: {
first: {
properties: { name: { type: "string", const: "first" }, title: { type: "number" } }
},
second: {
properties: { name: { type: "string", const: "second" }, title: { type: "number" } }
}
}
});
const res = reduceOneOfDeclarator({
node,
data: { name: "first", title: "not a number" },
pointer: "#",
path: []
});
assert(isSchemaNode(res), "expected result to be a node");
assert.deepEqual(res.schema, {
properties: { name: { type: "string", const: "first" }, title: { type: "number" } }
});
});
});
describe("array", () => {
// TODO test access on array-item as oneOfProperty id (oneOfProperty: "0")
});
});
describe("keyword : oneof-fuzzy : validate", () => {
it("should return one-of-error oneOfProperty does not match", () => {
const node = compileSchema(
{
type: "array",
items: {
oneOfProperty: "id",
oneOf: [
{
type: "object",
required: ["id"],
properties: { id: { const: "one" } }
},
{
type: "object",
required: ["id"],
properties: { id: { const: "two" } }
}
]
}
},
{ drafts: [draftEditor] }
);
const { errors } = node.validate([{ id: "unknown" }]);
assert.equal(errors.length, 1);
assert.deepEqual(errors[0].code, "one-of-error");
});
it("should return validation errors of object identified by oneOfProperty", () => {
const node = compileSchema(
{
type: "array",
items: {
oneOfProperty: "id",
oneOf: [
{
type: "object",
required: ["id"],
properties: {
id: { const: "one" },
title: { type: "string" }
}
}
]
}
},
{ drafts: [draftEditor] }
);
const { errors } = node.validate([{ id: "one", title: 123 }]);
assert.equal(errors.length, 1);
assert.deepEqual(errors[0].code, "type-error");
});
// issue json-editor
it("should return unique-items error for failed oneOf item", () => {
const node = compileSchema(
{
type: "object",
required: ["main"],
properties: {
main: {
type: "array",
items: {
oneOfProperty: "type",
oneOf: [{ $ref: "#/$defs/parent" }]
}
}
},
$defs: {
parent: {
type: "object",
title: "Parent",
description:
"Adding a duplicate item to this list fails as uniqueItems=true in children. @todo correct error message",
required: ["type", "children"],
properties: {
type: {
"x-options": { hidden: true },
type: "string",
const: "parent"
},
children: {
type: "array",
title: "Children",
uniqueItems: true,
items: {
oneOfProperty: "type",
oneOf: [
{
type: "object",
title: "Child: First",
required: ["type"],
properties: {
type: {
"x-options": { hidden: true },
type: "string",
const: "one"
}
}
},
{
type: "object",
title: "Child: Second",
required: ["type"],
properties: {
type: {
"x-options": { hidden: true },
type: "string",
const: "two"
}
}
}
]
}
}
}
}
}
},
{ drafts: [draftEditor] }
);
const { errors } = node.validate({
main: [{ type: "parent", children: [{ type: "one" }, { type: "one" }] }]
});
assert.equal(errors.length, 1);
assert.deepEqual(errors[0].data.pointer, "#/main/0/children/1");
assert.deepEqual(errors[0].code, "unique-items-error");
});
});