From 3b3f546b8a197f55163ddae18bb5d0e637017d0e Mon Sep 17 00:00:00 2001 From: Arpit Kuriyal Date: Tue, 24 Jun 2025 22:39:28 +0530 Subject: [PATCH 1/2] added options param in the main function --- src/index.d.ts | 10 +- src/index.js | 55 ++++++++- src/keywordErrorMessage.test.ts | 34 ++++++ src/normalizeOutputFormat/normalizeOutput.js | 32 ++---- .../normalizeOutput.test.js | 104 ++++++++---------- 5 files changed, 147 insertions(+), 88 deletions(-) create mode 100644 src/keywordErrorMessage.test.ts diff --git a/src/index.d.ts b/src/index.d.ts index da39df8..d090eca 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -1,13 +1,17 @@ export const betterJsonSchemaErrors: ( instance: Json, - schema: SchemaObject, - errorOutput: OutputFormat + errorOutput: OutputFormat, + options?: BetterJsonSchemaErrorsOptions ) => Promise; export type BetterJsonSchemaErrors = { errors: ErrorObject[]; }; +export type BetterJsonSchemaErrorsOptions = { + schemaUri?: string; +}; + export type ErrorObject = { schemaLocation: string; instanceLocation: string; @@ -31,6 +35,7 @@ export type OutputFormat = { export type OutputUnit = { valid?: boolean; + keyword?: string; absoluteKeywordLocation?: string; keywordLocation?: string; instanceLocation: string; @@ -40,6 +45,7 @@ export type OutputUnit = { export type NormalizedError = { valid: false; + keyword: string; absoluteKeywordLocation: string; instanceLocation: string; }; diff --git a/src/index.js b/src/index.js index 37a38c0..05a0ee9 100644 --- a/src/index.js +++ b/src/index.js @@ -1,17 +1,27 @@ import { normalizeOutputFormat } from "./normalizeOutputFormat/normalizeOutput.js"; +import * as Schema from "@hyperjump/browser"; +import { getSchema } from "@hyperjump/json-schema/experimental"; /** - * @import {betterJsonSchemaErrors} from "./index.d.ts" + * @import { Browser } from "@hyperjump/browser"; + * @import { SchemaDocument } from "@hyperjump/json-schema/experimental"; + * @import { Json } from "@hyperjump/json-pointer"; + * @import {betterJsonSchemaErrors, OutputUnit } from "./index.d.ts" */ /** @type betterJsonSchemaErrors */ -export async function betterJsonSchemaErrors(instance, schema, errorOutput) { - const normalizedErrors = await normalizeOutputFormat(errorOutput, schema); - +export async function betterJsonSchemaErrors(instance, errorOutput, options = {}) { + const normalizedErrors = await normalizeOutputFormat(errorOutput, options.schemaUri); const errors = []; for (const error of normalizedErrors) { + if (skip.has(error.keyword)) { + continue; + } + + /** @type Browser */ + const schema = await getSchema(error.absoluteKeywordLocation); errors.push({ - message: "The instance should be at least 3 characters", + message: getErrorMessage(error, schema, instance), instanceLocation: error.instanceLocation, schemaLocation: error.absoluteKeywordLocation }); @@ -19,3 +29,38 @@ export async function betterJsonSchemaErrors(instance, schema, errorOutput) { return { errors }; } + +/** @type (outputUnit: OutputUnit, schema: Browser, instance: Json) => string */ +const getErrorMessage = (outputUnit, schema) => { + if (outputUnit.keyword === "https://json-schema.org/keyword/minLength") { + return `The instance should be at least ${Schema.value(schema)} characters`; + } + + throw Error("TODO: Error message not implemented"); + // if (outputUnit.keyword === "https://json-schema.org/keyword/required") { + // const schemaDocument = await Schema.get(outputUnit.absoluteKeywordLocation); + // const required = new Set(Schema.value(schemaDocument)); + // const object = Instance.get(outputUnit.instanceLocation, instance); + // for (const propertyName of Instance.keys(object)) { + // required.delete(propertyName); + // } + + // return `"${outputUnit.instanceLocation}" is missing required property(s): ${[...required]}. Schema location: ${outputUnit.absoluteKeywordLocation}`; + // } else { + // // Default message + // return `"${outputUnit.instanceLocation}" fails schema constraint ${outputUnit.absoluteKeywordLocation}`; + // } +}; + +// These are probably not very useful for human readable messaging, so we'll skip them. +const skip = new Set([ + "https://json-schema.org/evaluation/validate", + "https://json-schema.org/keyword/ref", + "https://json-schema.org/keyword/properties", + "https://json-schema.org/keyword/patternProperties", + "https://json-schema.org/keyword/items", + "https://json-schema.org/keyword/prefixItems", + "https://json-schema.org/keyword/if", + "https://json-schema.org/keyword/then", + "https://json-schema.org/keyword/else" +]); diff --git a/src/keywordErrorMessage.test.ts b/src/keywordErrorMessage.test.ts new file mode 100644 index 0000000..24c1216 --- /dev/null +++ b/src/keywordErrorMessage.test.ts @@ -0,0 +1,34 @@ +import { describe, test, expect } from "vitest"; +import { betterJsonSchemaErrors } from "./index.js"; +import { registerSchema } from "@hyperjump/json-schema/draft-2020-12"; + +describe("Error messages", () => { + test("minLength", async () => { + registerSchema({ + $id: "https://example.com/main", + $schema: "https://json-schema.org/draft/2020-12/schema", + minLength: 3 + }); + + const instance = "aa"; + + /** @type OutputFormat */ + const output = { + valid: false, + errors: [ + { + absoluteKeywordLocation: "https://example.com/main#/minLength", + instanceLocation: "#" + } + ] + }; + + const result = await betterJsonSchemaErrors(instance, output); + expect(result.errors).to.eql([{ + schemaLocation: "https://example.com/main#/minLength", + instanceLocation: "#", + message: "The instance should be at least 3 characters" + } + ]); + }); +}); diff --git a/src/normalizeOutputFormat/normalizeOutput.js b/src/normalizeOutputFormat/normalizeOutput.js index da6e8e6..69260f1 100644 --- a/src/normalizeOutputFormat/normalizeOutput.js +++ b/src/normalizeOutputFormat/normalizeOutput.js @@ -1,8 +1,6 @@ import * as Browser from "@hyperjump/browser"; -import { registerSchema, unregisterSchema } from "@hyperjump/json-schema/draft-2020-12"; -import { getSchema } from "@hyperjump/json-schema/experimental"; +import { getSchema, getKeywordId } from "@hyperjump/json-schema/experimental"; import { pointerSegments } from "@hyperjump/json-pointer"; -import { randomUUID } from "crypto"; /** * @import { OutputFormat, OutputUnit, NormalizedError, SchemaObject} from "../index.d.ts"; @@ -12,10 +10,10 @@ import { randomUUID } from "crypto"; /** * @param {OutputFormat} errorOutput - * @param {SchemaObject} schema + * @param {string} [schemaUri] * @returns {Promise} */ -export async function normalizeOutputFormat(errorOutput, schema) { +export async function normalizeOutputFormat(errorOutput, schemaUri) { /** @type {NormalizedError[]} */ const output = []; @@ -39,7 +37,7 @@ export async function normalizeOutputFormat(errorOutput, schema) { } const absoluteKeywordLocation = error.absoluteKeywordLocation - ?? await toAbsoluteKeywordLocation(schema, /** @type string */ (error.keywordLocation)); + ?? await toAbsoluteKeywordLocation(/** @type string */ (schemaUri), /** @type string */ (error.keywordLocation)); const fragment = absoluteKeywordLocation.split("#")[1]; const lastSegment = fragment.split("/").filter(Boolean).pop(); @@ -48,6 +46,7 @@ export async function normalizeOutputFormat(errorOutput, schema) { if (lastSegment && keywords.has(lastSegment)) { output.push({ valid: false, + keyword: error.keyword ?? getKeywordId(lastSegment, "https://json-schema.org/draft/2020-12/schema"), absoluteKeywordLocation, instanceLocation: normalizeInstanceLocation(error.instanceLocation) }); @@ -78,22 +77,15 @@ function normalizeInstanceLocation(location) { /** * Convert keywordLocation to absoluteKeywordLocation - * @param {SchemaObject} schema + * @param {string} uri * @param {string} keywordLocation * @returns {Promise} */ -export async function toAbsoluteKeywordLocation(schema, keywordLocation) { - const uri = `urn:uuid:${randomUUID()}`; - try { - registerSchema(schema, uri); - - let browser = await getSchema(uri); - for (const segment of pointerSegments(keywordLocation)) { - browser = /** @type BrowserType */ (await Browser.step(segment, browser)); - } - - return `${browser.document.baseUri}#${browser.cursor}`; - } finally { - unregisterSchema(uri); +export async function toAbsoluteKeywordLocation(uri, keywordLocation) { + let browser = await getSchema(uri); + for (const segment of pointerSegments(keywordLocation)) { + browser = /** @type BrowserType */ (await Browser.step(segment, browser)); } + + return `${browser.document.baseUri}#${browser.cursor}`; } diff --git a/src/normalizeOutputFormat/normalizeOutput.test.js b/src/normalizeOutputFormat/normalizeOutput.test.js index 1b65441..71e697e 100644 --- a/src/normalizeOutputFormat/normalizeOutput.test.js +++ b/src/normalizeOutputFormat/normalizeOutput.test.js @@ -1,16 +1,18 @@ import { describe, expect, test } from "vitest"; import { normalizeOutputFormat } from "./normalizeOutput.js"; import { betterJsonSchemaErrors } from "../index.js"; +import { registerSchema, unregisterSchema } from "@hyperjump/json-schema/draft-2020-12"; /** - * @import { OutputFormat, OutputUnit, SchemaObject } from "../index.d.ts" + * @import { OutputFormat, OutputUnit} from "../index.d.ts" */ describe("Error Output Normalization", () => { test("Simple keyword with a standard Basic output format", async () => { - /** @type SchemaObject */ - const schema = { + const schemaUri = "https://example.com/main"; + registerSchema({ + $schema: "https://json-schema.org/draft/2020-12/schema", minLength: 3 - }; + }, schemaUri); const instance = "aa"; @@ -25,22 +27,22 @@ describe("Error Output Normalization", () => { ] }; - const result = await betterJsonSchemaErrors(instance, schema, output); + const result = await betterJsonSchemaErrors(instance, output); expect(result.errors).to.eql([{ schemaLocation: "https://example.com/main#/minLength", instanceLocation: "#", message: "The instance should be at least 3 characters" } ]); + unregisterSchema(schemaUri); }); test("Checking when output contain only instanceLocation and keywordLocation ", async () => { - /** @type SchemaObject */ - const schema = { - $id: "https://example.com/main", + const schemaUri = "https://example.com/main"; + registerSchema({ $schema: "https://json-schema.org/draft/2020-12/schema", minLength: 3 - }; + }, schemaUri); const instance = "aa"; @@ -55,20 +57,21 @@ describe("Error Output Normalization", () => { ] }; - const result = await betterJsonSchemaErrors(instance, schema, output); + const result = await betterJsonSchemaErrors(instance, output, { schemaUri }); expect(result.errors).to.eql([{ schemaLocation: "https://example.com/main#/minLength", instanceLocation: "#", message: "The instance should be at least 3 characters" }]); + unregisterSchema(schemaUri); }); test("adding # if instanceLocation doesn't have it", async () => { - /** @type SchemaObject */ - const schema = { - $id: "https://example.com/main", - minlength: 3 - }; + const schemaUri = "https://example.com/main"; + registerSchema({ + $schema: "https://json-schema.org/draft/2020-12/schema", + minLength: 3 + }, schemaUri); const instance = "aa"; @@ -84,31 +87,16 @@ describe("Error Output Normalization", () => { ] }; - const result = await betterJsonSchemaErrors(instance, schema, output); + const result = await betterJsonSchemaErrors(instance, output); expect(result.errors).to.eql([{ schemaLocation: "https://example.com/main#/minLength", instanceLocation: "#", message: "The instance should be at least 3 characters" }]); + unregisterSchema(schemaUri); }); - // const schema = { - // type: "object", - // properties: { - // foo: { $ref: "#/$defs/foo" } - // }, - // $defs: { - // foo: { type: "number" } - // } - // }; - // const instance = { foo: true }; - // const absoluteKeywordLocation = "/$defs/foo/type"; - // const keywordLocation = "/properties/foo/$ref/type"; - test("checking for the basic output format", async () => { - const schema = { - $id: "https://example.com/polygon" - }; const errorOutput = { valid: false, errors: [ @@ -136,14 +124,16 @@ describe("Error Output Normalization", () => { ] }; - expect(await normalizeOutputFormat(errorOutput, schema)).to.eql([ + expect(await normalizeOutputFormat(errorOutput)).to.eql([ { valid: false, + keyword: "https://json-schema.org/keyword/required", absoluteKeywordLocation: "https://example.com/polygon#/$defs/point/required", instanceLocation: "#/1" }, { valid: false, + keyword: "https://json-schema.org/keyword/additionalProperties", absoluteKeywordLocation: "https://example.com/polygon#/$defs/point/additionalProperties", instanceLocation: "#/1/z" } @@ -151,9 +141,6 @@ describe("Error Output Normalization", () => { }); test("checking for the detailed output format", async () => { - const schema = { - $id: "https://example.com/polygon" - }; const errorOutput = { valid: false, keywordLocation: "#", @@ -184,14 +171,16 @@ describe("Error Output Normalization", () => { ] }; - expect(await normalizeOutputFormat(errorOutput, schema)).to.eql([ + expect(await normalizeOutputFormat(errorOutput)).to.eql([ { valid: false, + keyword: "https://json-schema.org/keyword/required", absoluteKeywordLocation: "https://example.com/polygon#/$defs/point/required", instanceLocation: "#/1" }, { valid: false, + keyword: "https://json-schema.org/keyword/additionalProperties", absoluteKeywordLocation: "https://example.com/polygon#/$defs/point/additionalProperties", instanceLocation: "#/1/z" } @@ -199,9 +188,6 @@ describe("Error Output Normalization", () => { }); test("checking for the verbose output format", async () => { - const schema = { - $id: "https://example.com/polygon" - }; const errorOutput = { valid: false, keywordLocation: "#", @@ -209,22 +195,22 @@ describe("Error Output Normalization", () => { errors: [ { valid: true, - absoluteKeywordLocation: "https://example.com/schema#/type", + absoluteKeywordLocation: "https://example.com/main4#/type", instanceLocation: "#" }, { valid: true, - absoluteKeywordLocation: "https://example.com/schema#/properties", + absoluteKeywordLocation: "https://example.com/main4#/properties", instanceLocation: "#" }, { valid: false, - absoluteKeywordLocation: "https://example.com/schema#/additionalProperties", + absoluteKeywordLocation: "https://example.com/main4#/additionalProperties", instanceLocation: "#", errors: [ { valid: false, - absoluteKeywordLocation: "https://example.com/schema#/additionalProperties", + absoluteKeywordLocation: "https://example.com/main4#/additionalProperties", instanceLocation: "#/disallowedProp", error: "Additional property 'disallowedProp' found but was invalid." } @@ -233,24 +219,23 @@ describe("Error Output Normalization", () => { ] }; - expect(await normalizeOutputFormat(errorOutput, schema)).to.eql([ + expect(await normalizeOutputFormat(errorOutput)).to.eql([ { valid: false, - absoluteKeywordLocation: "https://example.com/schema#/additionalProperties", + keyword: "https://json-schema.org/keyword/additionalProperties", + absoluteKeywordLocation: "https://example.com/main4#/additionalProperties", instanceLocation: "#" }, { valid: false, - absoluteKeywordLocation: "https://example.com/schema#/additionalProperties", + keyword: "https://json-schema.org/keyword/additionalProperties", + absoluteKeywordLocation: "https://example.com/main4#/additionalProperties", instanceLocation: "#/disallowedProp" } ]); }); test("when error output doesnot contain any of these three keyword (valid, absoluteKeywordLocation, instanceLocation)", async () => { - const schema = { - $id: "https://example.com/polygon" - }; const errorOutput = { valid: false, errors: [ @@ -260,13 +245,12 @@ describe("Error Output Normalization", () => { } ] }; - await expect(async () => normalizeOutputFormat(/** @type any */(errorOutput), schema)).to.rejects.toThrow("error Output must follow Draft 2019-09"); + await expect(async () => normalizeOutputFormat(/** @type any */(errorOutput))).to.rejects.toThrow("error Output must follow Draft 2019-09"); }); test("correctly resolves keywordLocation through $ref in $defs", async () => { - /** @type SchemaObject */ - const schema = { - $id: "https://example.com/main", + const schemaUri = "https://example.com/main"; + registerSchema({ $schema: "https://json-schema.org/draft/2020-12/schema", properties: { foo: { $ref: "#/$defs/lengthDefinition" } @@ -276,7 +260,7 @@ describe("Error Output Normalization", () => { minLength: 3 } } - }; + }, schemaUri); const instance = { foo: "aa" }; /** @type OutputFormat */ const output = { @@ -288,8 +272,7 @@ describe("Error Output Normalization", () => { } ] }; - - const result = await betterJsonSchemaErrors(instance, schema, output); + const result = await betterJsonSchemaErrors(instance, output, { schemaUri }); expect(result.errors).to.eql([ { schemaLocation: "https://example.com/main#/$defs/lengthDefinition/minLength", @@ -297,12 +280,10 @@ describe("Error Output Normalization", () => { message: "The instance should be at least 3 characters" } ]); + unregisterSchema(schemaUri); }); test("removes schemaLocation nodes from the error output", async () => { - const schema = { - $id: "https://example.com/polygon" - }; const errorOutput = { valid: false, errors: [ @@ -323,9 +304,10 @@ describe("Error Output Normalization", () => { ] }; - expect(await normalizeOutputFormat(errorOutput, schema)).to.eql([ + expect(await normalizeOutputFormat(errorOutput)).to.eql([ { valid: false, + keyword: "https://json-schema.org/keyword/required", absoluteKeywordLocation: "https://example.com/polygon#/$defs/point/required", instanceLocation: "#/1" } From d72126d135c7c3a1f71aed6edf8411e288ba411b Mon Sep 17 00:00:00 2001 From: Arpit Kuriyal Date: Fri, 27 Jun 2025 23:07:52 +0530 Subject: [PATCH 2/2] completed betterErrors for simple keywords --- package-lock.json | 15 +- package.json | 3 +- src/index.d.ts | 6 +- src/index.js | 118 ++++-- src/keywordErrorMessage.test.js | 385 ++++++++++++++++++ src/keywordErrorMessage.test.ts | 34 -- src/normalizeOutputFormat/normalizeOutput.js | 92 ++--- .../normalizeOutput.test.js | 116 +++++- 8 files changed, 631 insertions(+), 138 deletions(-) create mode 100644 src/keywordErrorMessage.test.js delete mode 100644 src/keywordErrorMessage.test.ts diff --git a/package-lock.json b/package-lock.json index 1bd5672..dff80e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,8 @@ "license": "MIT", "dependencies": { "@hyperjump/browser": "^1.3.1", - "@hyperjump/json-schema": "^1.16.0" + "@hyperjump/json-schema": "^1.16.0", + "leven": "^4.0.0" }, "devDependencies": { "@stylistic/eslint-plugin": "*", @@ -4159,6 +4160,18 @@ "json-buffer": "3.0.1" } }, + "node_modules/leven": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-4.0.0.tgz", + "integrity": "sha512-puehA3YKku3osqPlNuzGDUHq8WpwXupUg1V6NXdV38G+gr+gkBwFC8g1b/+YcIvp8gnqVIus+eJCH/eGsRmJNw==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", diff --git a/package.json b/package.json index 071582e..e11453c 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ }, "dependencies": { "@hyperjump/browser": "^1.3.1", - "@hyperjump/json-schema": "^1.16.0" + "@hyperjump/json-schema": "^1.16.0", + "leven": "^4.0.0" } } diff --git a/src/index.d.ts b/src/index.d.ts index d090eca..0e7e730 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -1,17 +1,13 @@ export const betterJsonSchemaErrors: ( instance: Json, errorOutput: OutputFormat, - options?: BetterJsonSchemaErrorsOptions + schemaUri: string ) => Promise; export type BetterJsonSchemaErrors = { errors: ErrorObject[]; }; -export type BetterJsonSchemaErrorsOptions = { - schemaUri?: string; -}; - export type ErrorObject = { schemaLocation: string; instanceLocation: string; diff --git a/src/index.js b/src/index.js index 05a0ee9..3a10ef6 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,8 @@ import { normalizeOutputFormat } from "./normalizeOutputFormat/normalizeOutput.js"; import * as Schema from "@hyperjump/browser"; -import { getSchema } from "@hyperjump/json-schema/experimental"; +import { getSchema, getKeyword } from "@hyperjump/json-schema/experimental"; +import * as Instance from "@hyperjump/json-pointer"; +import leven from "leven"; /** * @import { Browser } from "@hyperjump/browser"; @@ -10,11 +12,13 @@ import { getSchema } from "@hyperjump/json-schema/experimental"; */ /** @type betterJsonSchemaErrors */ -export async function betterJsonSchemaErrors(instance, errorOutput, options = {}) { - const normalizedErrors = await normalizeOutputFormat(errorOutput, options.schemaUri); +export async function betterJsonSchemaErrors(instance, errorOutput, schemaUri) { + const schema = await getSchema(schemaUri); + const normalizedErrors = await normalizeOutputFormat(errorOutput, schema); const errors = []; for (const error of normalizedErrors) { - if (skip.has(error.keyword)) { + const keywordHandler = getKeyword(error.keyword); + if (keywordHandler.simpleApplicator) { continue; } @@ -31,36 +35,88 @@ export async function betterJsonSchemaErrors(instance, errorOutput, options = {} } /** @type (outputUnit: OutputUnit, schema: Browser, instance: Json) => string */ -const getErrorMessage = (outputUnit, schema) => { +const getErrorMessage = (outputUnit, schema, instance) => { if (outputUnit.keyword === "https://json-schema.org/keyword/minLength") { return `The instance should be at least ${Schema.value(schema)} characters`; } + if (outputUnit.keyword === "https://json-schema.org/keyword/maxLength") { + return `The instance should be atmost ${Schema.value(schema)} characters long.`; + } + + if (outputUnit.keyword === "https://json-schema.org/keyword/type") { + const pointer = outputUnit.instanceLocation.replace(/^#/, ""); + const actualValue = Instance.get(pointer, instance); + return `The instance should be of type "${Schema.value(schema)}" but found "${typeof actualValue}".`; + } + + if (outputUnit.keyword === "https://json-schema.org/keyword/maximum") { + return `The instance should be less than or equal to ${Schema.value(schema)}.`; + } + + if (outputUnit.keyword === "https://json-schema.org/keyword/minimum") { + return `The instance should be greater than or equal to ${Schema.value(schema)}.`; + } + + if (outputUnit.keyword === "https://json-schema.org/keyword/exclusiveMaximum") { + return `The instance should be less than ${Schema.value(schema)}.`; + } + + if (outputUnit.keyword === "https://json-schema.org/keyword/exclusiveMinimum") { + return `The instance should be greater than ${Schema.value(schema)}.`; + } + + if (outputUnit.keyword === "https://json-schema.org/keyword/required") { + /** @type {Set} */ + const required = new Set(Schema.value(schema)); + const pointer = outputUnit.instanceLocation.replace(/^#/, ""); + const object = /** @type Object */ (Instance.get(pointer, instance)); + for (const propertyName of Object.keys(object)) { + required.delete(propertyName); + } + + return `"${outputUnit.instanceLocation}" is missing required property(s): ${[...required].join(", ")}.`; + } + + if (outputUnit.keyword === "https://json-schema.org/keyword/multipleOf") { + return `The instance should be of multiple of ${Schema.value(schema)}.`; + } + + if (outputUnit.keyword === "https://json-schema.org/keyword/maxProperties") { + return `The instance should have maximum ${Schema.value(schema)} properties.`; + } + + if (outputUnit.keyword === "https://json-schema.org/keyword/minProperties") { + return `The instance should have minimum ${Schema.value(schema)} properties.`; + } + + if (outputUnit.keyword === "https://json-schema.org/keyword/const") { + return `The instance should be equal to ${Schema.value(schema)}.`; + } + + if (outputUnit.keyword === "https://json-schema.org/keyword/enum") { + /** @type {Array} */ + const allowedValues = Schema.value(schema); + const pointer = outputUnit.instanceLocation.replace(/^#/, ""); + const currentValue = /** @type {string} */ (Instance.get(pointer, instance)); + + const bestMatch = allowedValues + .map((value) => ({ + value, + weight: leven(value, currentValue) + })) + .sort((a, b) => a.weight - b.weight)[0]; + + let suggestion = ""; + if ( + allowedValues.length === 1 + || (bestMatch && bestMatch.weight < bestMatch.value.length) + ) { + suggestion = ` Did you mean "${bestMatch.value}"?`; + return `Unexpected value "${currentValue}". ${suggestion}`; + } + + return `Unexpected value "${currentValue}". Expected one of: ${allowedValues.join(",")}.`; + } throw Error("TODO: Error message not implemented"); - // if (outputUnit.keyword === "https://json-schema.org/keyword/required") { - // const schemaDocument = await Schema.get(outputUnit.absoluteKeywordLocation); - // const required = new Set(Schema.value(schemaDocument)); - // const object = Instance.get(outputUnit.instanceLocation, instance); - // for (const propertyName of Instance.keys(object)) { - // required.delete(propertyName); - // } - - // return `"${outputUnit.instanceLocation}" is missing required property(s): ${[...required]}. Schema location: ${outputUnit.absoluteKeywordLocation}`; - // } else { - // // Default message - // return `"${outputUnit.instanceLocation}" fails schema constraint ${outputUnit.absoluteKeywordLocation}`; - // } }; - -// These are probably not very useful for human readable messaging, so we'll skip them. -const skip = new Set([ - "https://json-schema.org/evaluation/validate", - "https://json-schema.org/keyword/ref", - "https://json-schema.org/keyword/properties", - "https://json-schema.org/keyword/patternProperties", - "https://json-schema.org/keyword/items", - "https://json-schema.org/keyword/prefixItems", - "https://json-schema.org/keyword/if", - "https://json-schema.org/keyword/then", - "https://json-schema.org/keyword/else" -]); diff --git a/src/keywordErrorMessage.test.js b/src/keywordErrorMessage.test.js new file mode 100644 index 0000000..df39082 --- /dev/null +++ b/src/keywordErrorMessage.test.js @@ -0,0 +1,385 @@ +import { afterEach, describe, test, expect } from "vitest"; +import { betterJsonSchemaErrors } from "./index.js"; +import { registerSchema } from "@hyperjump/json-schema/draft-2020-12"; +import { unregisterSchema } from "@hyperjump/json-schema"; + +/** + * @import { OutputFormat} from "./index.d.ts" + */ + +describe("Error messages", () => { + const schemaUri = "https://example.com/main"; + + afterEach(() => { + unregisterSchema(schemaUri); + }); + + test("minLength", async () => { + registerSchema({ + $schema: "https://json-schema.org/draft/2020-12/schema", + minLength: 3 + }, schemaUri); + + const instance = "aa"; + + /** @type OutputFormat */ + const output = { + valid: false, + errors: [ + { + absoluteKeywordLocation: "https://example.com/main#/minLength", + instanceLocation: "#" + } + ] + }; + + const result = await betterJsonSchemaErrors(instance, output, schemaUri); + expect(result.errors).to.eql([{ + schemaLocation: "https://example.com/main#/minLength", + instanceLocation: "#", + message: "The instance should be at least 3 characters" + } + ]); + }); + + test("maxLength", async () => { + registerSchema({ + $schema: "https://json-schema.org/draft/2020-12/schema", + maxLength: 3 + }, schemaUri); + + const instance = "aaaa"; + + /** @type OutputFormat */ + const output = { + valid: false, + errors: [ + { + absoluteKeywordLocation: "https://example.com/main#/maxLength", + instanceLocation: "#" + } + ] + }; + + const result = await betterJsonSchemaErrors(instance, output, schemaUri); + expect(result.errors).to.eql([{ + schemaLocation: "https://example.com/main#/maxLength", + instanceLocation: "#", + message: "The instance should be atmost 3 characters long." + } + ]); + }); + + test("type", async () => { + registerSchema({ + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "number" + }, schemaUri); + + const instance = "aaaa"; + + /** @type OutputFormat */ + const output = { + valid: false, + errors: [ + { + absoluteKeywordLocation: "https://example.com/main#/type", + instanceLocation: "#" + } + ] + }; + + const result = await betterJsonSchemaErrors(instance, output, schemaUri); + expect(result.errors).to.eql([{ + schemaLocation: "https://example.com/main#/type", + instanceLocation: "#", + message: `The instance should be of type "number" but found "string".` + } + ]); + }); + + test("maxmimum", async () => { + registerSchema({ + $schema: "https://json-schema.org/draft/2020-12/schema", + maximum: 10 + }, schemaUri); + + const instance = 11; + + /** @type OutputFormat */ + const output = { + valid: false, + errors: [ + { + absoluteKeywordLocation: "https://example.com/main#/maximum", + instanceLocation: "#" + } + ] + }; + + const result = await betterJsonSchemaErrors(instance, output, schemaUri); + expect(result.errors).to.eql([{ + schemaLocation: "https://example.com/main#/maximum", + instanceLocation: "#", + message: `The instance should be less than or equal to 10.` + } + ]); + }); + + test("mimimum", async () => { + registerSchema({ + $schema: "https://json-schema.org/draft/2020-12/schema", + minimum: 10 + }, schemaUri); + + const instance = 9.9; + + /** @type OutputFormat */ + const output = { + valid: false, + errors: [ + { + absoluteKeywordLocation: "https://example.com/main#/minimum", + instanceLocation: "#" + } + ] + }; + + const result = await betterJsonSchemaErrors(instance, output, schemaUri); + expect(result.errors).to.eql([{ + schemaLocation: "https://example.com/main#/minimum", + instanceLocation: "#", + message: `The instance should be greater than or equal to 10.` + } + ]); + }); + + test("exclusiveMaximum", async () => { + registerSchema({ + $schema: "https://json-schema.org/draft/2020-12/schema", + exclusiveMaximum: 10 + }, schemaUri); + + const instance = 11; + + /** @type OutputFormat */ + const output = { + valid: false, + errors: [ + { + absoluteKeywordLocation: "https://example.com/main#/exclusiveMaximum", + instanceLocation: "#" + } + ] + }; + + const result = await betterJsonSchemaErrors(instance, output, schemaUri); + expect(result.errors).to.eql([{ + schemaLocation: "https://example.com/main#/exclusiveMaximum", + instanceLocation: "#", + message: `The instance should be less than 10.` + } + ]); + }); + + test("exclusiveMinimum", async () => { + registerSchema({ + $schema: "https://json-schema.org/draft/2020-12/schema", + exclusiveMinimum: 10 + }, schemaUri); + + const instance = 9; + + /** @type OutputFormat */ + const output = { + valid: false, + errors: [ + { + absoluteKeywordLocation: "https://example.com/main#/exclusiveMinimum", + instanceLocation: "#" + } + ] + }; + + const result = await betterJsonSchemaErrors(instance, output, schemaUri); + expect(result.errors).to.eql([{ + schemaLocation: "https://example.com/main#/exclusiveMinimum", + instanceLocation: "#", + message: `The instance should be greater than 10.` + } + ]); + }); + + test("required", async () => { + registerSchema({ + $schema: "https://json-schema.org/draft/2020-12/schema", + required: ["foo", "bar", "baz"] + + }, schemaUri); + + const instance = { foo: 1, bar: 2, extra: true }; + + /** @type OutputFormat */ + const output = { + valid: false, + errors: [ + { + absoluteKeywordLocation: "https://example.com/main#/required", + instanceLocation: "#" + } + ] + }; + + const result = await betterJsonSchemaErrors(instance, output, schemaUri); + expect(result.errors).to.eql([{ + schemaLocation: "https://example.com/main#/required", + instanceLocation: "#", + message: `"#" is missing required property(s): baz.` + } + ]); + }); + + test("multipleOf", async () => { + registerSchema({ + $schema: "https://json-schema.org/draft/2020-12/schema", + multipleOf: 5 + + }, schemaUri); + + const instance = 11; + + /** @type OutputFormat */ + const output = { + valid: false, + errors: [ + { + absoluteKeywordLocation: "https://example.com/main#/multipleOf", + instanceLocation: "#" + } + ] + }; + + const result = await betterJsonSchemaErrors(instance, output, schemaUri); + expect(result.errors).to.eql([{ + schemaLocation: "https://example.com/main#/multipleOf", + instanceLocation: "#", + message: `The instance should be of multiple of 5.` + } + ]); + }); + + test("maxProperties", async () => { + registerSchema({ + $schema: "https://json-schema.org/draft/2020-12/schema", + maxProperties: 2 + + }, schemaUri); + + const instance = { foo: 1, bar: 2, baz: 3 }; + + /** @type OutputFormat */ + const output = { + valid: false, + errors: [ + { + absoluteKeywordLocation: "https://example.com/main#/maxProperties", + instanceLocation: "#" + } + ] + }; + + const result = await betterJsonSchemaErrors(instance, output, schemaUri); + expect(result.errors).to.eql([{ + schemaLocation: "https://example.com/main#/maxProperties", + instanceLocation: "#", + message: `The instance should have maximum 2 properties.` + } + ]); + }); + + test("minProperties", async () => { + registerSchema({ + $schema: "https://json-schema.org/draft/2020-12/schema", + minProperties: 2 + + }, schemaUri); + + const instance = { foo: 1 }; + + /** @type OutputFormat */ + const output = { + valid: false, + errors: [ + { + absoluteKeywordLocation: "https://example.com/main#/minProperties", + instanceLocation: "#" + } + ] + }; + + const result = await betterJsonSchemaErrors(instance, output, schemaUri); + expect(result.errors).to.eql([{ + schemaLocation: "https://example.com/main#/minProperties", + instanceLocation: "#", + message: `The instance should have minimum 2 properties.` + } + ]); + }); + + test("const", async () => { + registerSchema({ + $schema: "https://json-schema.org/draft/2020-12/schema", + const: 2 + + }, schemaUri); + + const instance = 3; + + /** @type OutputFormat */ + const output = { + valid: false, + errors: [ + { + absoluteKeywordLocation: "https://example.com/main#/const", + instanceLocation: "#" + } + ] + }; + + const result = await betterJsonSchemaErrors(instance, output, schemaUri); + expect(result.errors).to.eql([{ + schemaLocation: "https://example.com/main#/const", + instanceLocation: "#", + message: `The instance should be equal to 2.` + } + ]); + }); + + test("enum", async () => { + registerSchema({ + $schema: "https://json-schema.org/draft/2020-12/schema", + enum: ["red", "green", "blue"] + + }, schemaUri); + + const instance = "rwd"; + + /** @type OutputFormat */ + const output = { + valid: false, + errors: [ + { + absoluteKeywordLocation: "https://example.com/main#/enum", + instanceLocation: "#" + } + ] + }; + + const result = await betterJsonSchemaErrors(instance, output, schemaUri); + expect(result.errors).to.eql([{ + schemaLocation: "https://example.com/main#/enum", + instanceLocation: "#", + message: `Unexpected value "rwd". Did you mean "red"?` + }]); + }); +}); diff --git a/src/keywordErrorMessage.test.ts b/src/keywordErrorMessage.test.ts deleted file mode 100644 index 24c1216..0000000 --- a/src/keywordErrorMessage.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { describe, test, expect } from "vitest"; -import { betterJsonSchemaErrors } from "./index.js"; -import { registerSchema } from "@hyperjump/json-schema/draft-2020-12"; - -describe("Error messages", () => { - test("minLength", async () => { - registerSchema({ - $id: "https://example.com/main", - $schema: "https://json-schema.org/draft/2020-12/schema", - minLength: 3 - }); - - const instance = "aa"; - - /** @type OutputFormat */ - const output = { - valid: false, - errors: [ - { - absoluteKeywordLocation: "https://example.com/main#/minLength", - instanceLocation: "#" - } - ] - }; - - const result = await betterJsonSchemaErrors(instance, output); - expect(result.errors).to.eql([{ - schemaLocation: "https://example.com/main#/minLength", - instanceLocation: "#", - message: "The instance should be at least 3 characters" - } - ]); - }); -}); diff --git a/src/normalizeOutputFormat/normalizeOutput.js b/src/normalizeOutputFormat/normalizeOutput.js index 69260f1..39f9af9 100644 --- a/src/normalizeOutputFormat/normalizeOutput.js +++ b/src/normalizeOutputFormat/normalizeOutput.js @@ -1,19 +1,24 @@ import * as Browser from "@hyperjump/browser"; -import { getSchema, getKeywordId } from "@hyperjump/json-schema/experimental"; +import { getKeywordByName } from "@hyperjump/json-schema/experimental"; import { pointerSegments } from "@hyperjump/json-pointer"; /** - * @import { OutputFormat, OutputUnit, NormalizedError, SchemaObject} from "../index.d.ts"; + * @import { + * OutputFormat, + * OutputUnit, + * NormalizedError, + * SchemaObject + * } from "../index.d.ts"; * @import { SchemaDocument } from "@hyperjump/json-schema/experimental"; * @import { Browser as BrowserType } from "@hyperjump/browser"; */ /** * @param {OutputFormat} errorOutput - * @param {string} [schemaUri] + * @param {BrowserType} schema * @returns {Promise} */ -export async function normalizeOutputFormat(errorOutput, schemaUri) { +export async function normalizeOutputFormat(errorOutput, schema) { /** @type {NormalizedError[]} */ const output = []; @@ -21,53 +26,47 @@ export async function normalizeOutputFormat(errorOutput, schemaUri) { throw new Error("error Output must follow Draft 2019-09"); } - const keywords = new Set([ - "type", "minLength", "maxLength", "minimum", "maximum", "format", "pattern", - "enum", "const", "required", "items", "properties", "allOf", "anyOf", "oneOf", - "not", "contains", "uniqueItems", "additionalProperties", "minItems", "maxItems", - "minProperties", "maxProperties", "dependentRequired", "dependencies" - ]); - - /** @type {(errorOutput: OutputUnit) => Promise} */ - async function collectErrors(error) { - if (error.valid) return; - - if (!("instanceLocation" in error) || !("absoluteKeywordLocation" in error || "keywordLocation" in error)) { - throw new Error("error Output must follow Draft 2019-09"); - } - - const absoluteKeywordLocation = error.absoluteKeywordLocation - ?? await toAbsoluteKeywordLocation(/** @type string */ (schemaUri), /** @type string */ (error.keywordLocation)); + if (!errorOutput.errors) { + throw new Error("error Output must follow Draft 2019-09"); + } - const fragment = absoluteKeywordLocation.split("#")[1]; - const lastSegment = fragment.split("/").filter(Boolean).pop(); + for (const err of errorOutput.errors) { + await collectErrors(err, output, schema); + } - // make a check here to remove the schemaLocation. - if (lastSegment && keywords.has(lastSegment)) { - output.push({ - valid: false, - keyword: error.keyword ?? getKeywordId(lastSegment, "https://json-schema.org/draft/2020-12/schema"), - absoluteKeywordLocation, - instanceLocation: normalizeInstanceLocation(error.instanceLocation) - }); - } + return output; +} - if (error.errors) { - for (const nestedError of error.errors) { - await collectErrors(nestedError); // Recursive - } - } - } +/** @type {(errorOutput: OutputUnit, output: NormalizedError[], schema: BrowserType) => Promise} */ +async function collectErrors(error, output, schema) { + if (error.valid) return; - if (!errorOutput.errors) { + if (!("instanceLocation" in error) || !("absoluteKeywordLocation" in error || "keywordLocation" in error)) { throw new Error("error Output must follow Draft 2019-09"); } - for (const err of errorOutput.errors) { - await collectErrors(err); + const absoluteKeywordLocation = error.absoluteKeywordLocation + ?? await toAbsoluteKeywordLocation(schema, /** @type string */ (error.keywordLocation)); + + const fragment = absoluteKeywordLocation.split("#")[1]; + const lastSegment = fragment.split("/").filter(Boolean).pop(); + const keywordHandler = getKeywordByName(/** @type string */ (lastSegment), schema.document.dialectId); + + // make a check here to remove the schemaLocation. + if (lastSegment && !keywordHandler.id.startsWith("https://json-schema.org/keyword/unknown")) { + output.push({ + valid: false, + keyword: error.keyword ?? keywordHandler.id, + absoluteKeywordLocation, + instanceLocation: normalizeInstanceLocation(error.instanceLocation) + }); } - return output; + if (error.errors) { + for (const nestedError of error.errors) { + await collectErrors(nestedError, output, schema); // Recursive + } + } } /** @type {(location: string) => string} */ @@ -77,15 +76,14 @@ function normalizeInstanceLocation(location) { /** * Convert keywordLocation to absoluteKeywordLocation - * @param {string} uri + * @param {BrowserType} schema * @param {string} keywordLocation * @returns {Promise} */ -export async function toAbsoluteKeywordLocation(uri, keywordLocation) { - let browser = await getSchema(uri); +export async function toAbsoluteKeywordLocation(schema, keywordLocation) { for (const segment of pointerSegments(keywordLocation)) { - browser = /** @type BrowserType */ (await Browser.step(segment, browser)); + schema = /** @type BrowserType */ (await Browser.step(segment, schema)); } - return `${browser.document.baseUri}#${browser.cursor}`; + return `${schema.document.baseUri}#${schema.cursor}`; } diff --git a/src/normalizeOutputFormat/normalizeOutput.test.js b/src/normalizeOutputFormat/normalizeOutput.test.js index 71e697e..d280538 100644 --- a/src/normalizeOutputFormat/normalizeOutput.test.js +++ b/src/normalizeOutputFormat/normalizeOutput.test.js @@ -1,14 +1,22 @@ -import { describe, expect, test } from "vitest"; +import { afterEach, describe, expect, test } from "vitest"; import { normalizeOutputFormat } from "./normalizeOutput.js"; import { betterJsonSchemaErrors } from "../index.js"; import { registerSchema, unregisterSchema } from "@hyperjump/json-schema/draft-2020-12"; +import { getSchema } from "@hyperjump/json-schema/experimental"; /** - * @import { OutputFormat, OutputUnit} from "../index.d.ts" + * @import { OutputFormat} from "../index.d.ts" */ describe("Error Output Normalization", () => { + const schemaUri = "https://example.com/main"; + const schemaUri1 = "https://example.com/polygon"; + + afterEach(() => { + unregisterSchema(schemaUri); + unregisterSchema(schemaUri1); + }); + test("Simple keyword with a standard Basic output format", async () => { - const schemaUri = "https://example.com/main"; registerSchema({ $schema: "https://json-schema.org/draft/2020-12/schema", minLength: 3 @@ -27,18 +35,16 @@ describe("Error Output Normalization", () => { ] }; - const result = await betterJsonSchemaErrors(instance, output); + const result = await betterJsonSchemaErrors(instance, output, schemaUri); expect(result.errors).to.eql([{ schemaLocation: "https://example.com/main#/minLength", instanceLocation: "#", message: "The instance should be at least 3 characters" } ]); - unregisterSchema(schemaUri); }); test("Checking when output contain only instanceLocation and keywordLocation ", async () => { - const schemaUri = "https://example.com/main"; registerSchema({ $schema: "https://json-schema.org/draft/2020-12/schema", minLength: 3 @@ -57,17 +63,15 @@ describe("Error Output Normalization", () => { ] }; - const result = await betterJsonSchemaErrors(instance, output, { schemaUri }); + const result = await betterJsonSchemaErrors(instance, output, schemaUri); expect(result.errors).to.eql([{ schemaLocation: "https://example.com/main#/minLength", instanceLocation: "#", message: "The instance should be at least 3 characters" }]); - unregisterSchema(schemaUri); }); test("adding # if instanceLocation doesn't have it", async () => { - const schemaUri = "https://example.com/main"; registerSchema({ $schema: "https://json-schema.org/draft/2020-12/schema", minLength: 3 @@ -87,16 +91,33 @@ describe("Error Output Normalization", () => { ] }; - const result = await betterJsonSchemaErrors(instance, output); + const result = await betterJsonSchemaErrors(instance, output, schemaUri); expect(result.errors).to.eql([{ schemaLocation: "https://example.com/main#/minLength", instanceLocation: "#", message: "The instance should be at least 3 characters" }]); - unregisterSchema(schemaUri); }); test("checking for the basic output format", async () => { + const schemaUri = "https://example.com/polygon"; + registerSchema({ + $schema: "https://json-schema.org/draft/2020-12/schema", + $defs: { + point: { + type: "object", + properties: { + x: { type: "number" }, + y: { type: "number" } + }, + additionalProperties: false, + required: ["x", "y"] + } + }, + type: "array", + items: { $ref: "#/$defs/point" } + }, schemaUri); + const errorOutput = { valid: false, errors: [ @@ -124,7 +145,8 @@ describe("Error Output Normalization", () => { ] }; - expect(await normalizeOutputFormat(errorOutput)).to.eql([ + const schema = await getSchema(schemaUri); + expect(await normalizeOutputFormat(errorOutput, schema)).to.eql([ { valid: false, keyword: "https://json-schema.org/keyword/required", @@ -141,6 +163,24 @@ describe("Error Output Normalization", () => { }); test("checking for the detailed output format", async () => { + const schemaUri = "https://example.com/polygon"; + registerSchema({ + $schema: "https://json-schema.org/draft/2020-12/schema", + $defs: { + point: { + type: "object", + properties: { + x: { type: "number" }, + y: { type: "number" } + }, + additionalProperties: false, + required: ["x", "y"] + } + }, + type: "array", + items: { $ref: "#/$defs/point" } + }, schemaUri); + const errorOutput = { valid: false, keywordLocation: "#", @@ -171,7 +211,8 @@ describe("Error Output Normalization", () => { ] }; - expect(await normalizeOutputFormat(errorOutput)).to.eql([ + const schema = await getSchema(schemaUri); + expect(await normalizeOutputFormat(errorOutput, schema)).to.eql([ { valid: false, keyword: "https://json-schema.org/keyword/required", @@ -188,6 +229,13 @@ describe("Error Output Normalization", () => { }); test("checking for the verbose output format", async () => { + registerSchema({ + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: {}, + additionalProperties: false + }, schemaUri); + const errorOutput = { valid: false, keywordLocation: "#", @@ -219,7 +267,8 @@ describe("Error Output Normalization", () => { ] }; - expect(await normalizeOutputFormat(errorOutput)).to.eql([ + const schema = await getSchema(schemaUri); + expect(await normalizeOutputFormat(errorOutput, schema)).to.eql([ { valid: false, keyword: "https://json-schema.org/keyword/additionalProperties", @@ -236,6 +285,16 @@ describe("Error Output Normalization", () => { }); test("when error output doesnot contain any of these three keyword (valid, absoluteKeywordLocation, instanceLocation)", async () => { + registerSchema({ + $schema: "https://json-schema.org/draft/2020-12/schema", + items: { $ref: "#/$defs/foo" }, + $defs: { + foo: { + additionalProperties: false + } + } + }, schemaUri); + const errorOutput = { valid: false, errors: [ @@ -245,11 +304,12 @@ describe("Error Output Normalization", () => { } ] }; - await expect(async () => normalizeOutputFormat(/** @type any */(errorOutput))).to.rejects.toThrow("error Output must follow Draft 2019-09"); + + const schema = await getSchema(schemaUri); + await expect(async () => normalizeOutputFormat(/** @type any */(errorOutput), schema)).to.rejects.toThrow("error Output must follow Draft 2019-09"); }); test("correctly resolves keywordLocation through $ref in $defs", async () => { - const schemaUri = "https://example.com/main"; registerSchema({ $schema: "https://json-schema.org/draft/2020-12/schema", properties: { @@ -272,7 +332,7 @@ describe("Error Output Normalization", () => { } ] }; - const result = await betterJsonSchemaErrors(instance, output, { schemaUri }); + const result = await betterJsonSchemaErrors(instance, output, schemaUri); expect(result.errors).to.eql([ { schemaLocation: "https://example.com/main#/$defs/lengthDefinition/minLength", @@ -280,10 +340,27 @@ describe("Error Output Normalization", () => { message: "The instance should be at least 3 characters" } ]); - unregisterSchema(schemaUri); }); test("removes schemaLocation nodes from the error output", async () => { + registerSchema({ + $schema: "https://json-schema.org/draft/2020-12/schema", + $defs: { + point: { + type: "object", + properties: { + x: { type: "number" }, + y: { type: "number" } + }, + additionalProperties: false, + required: ["x", "y"] + } + }, + type: "array", + items: { $ref: "#/$defs/point" }, + minItems: 3 + }, schemaUri); + const errorOutput = { valid: false, errors: [ @@ -304,7 +381,8 @@ describe("Error Output Normalization", () => { ] }; - expect(await normalizeOutputFormat(errorOutput)).to.eql([ + const schema = await getSchema(schemaUri); + expect(await normalizeOutputFormat(errorOutput, schema)).to.eql([ { valid: false, keyword: "https://json-schema.org/keyword/required",