From 7258839bb636e82932815bcb773c202c932006b3 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 3 Feb 2026 21:19:52 +0530 Subject: [PATCH 1/2] combined required and dependentRequired error messages --- src/error-handlers/dependentRequired.js | 29 ++++++++--- src/error-handlers/required.js | 66 +++++++++++++++++++------ src/test-suite/tests/dependencies.json | 15 ++---- src/test-suite/tests/required.json | 24 ++++++++- 4 files changed, 98 insertions(+), 36 deletions(-) diff --git a/src/error-handlers/dependentRequired.js b/src/error-handlers/dependentRequired.js index d149a85..f15fdf7 100644 --- a/src/error-handlers/dependentRequired.js +++ b/src/error-handlers/dependentRequired.js @@ -8,11 +8,24 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; /** @type ErrorHandler */ const dependentRequiredErrorHandler = async (normalizedErrors, instance, localization) => { + if (normalizedErrors["https://json-schema.org/keyword/required"]) { + for (const schemaLocation in normalizedErrors["https://json-schema.org/keyword/required"]) { + if (normalizedErrors["https://json-schema.org/keyword/required"][schemaLocation] === false) { + return []; + } + } + } + /** @type ErrorObject[] */ const errors = []; - for (const schemaLocation in normalizedErrors["https://json-schema.org/keyword/dependentRequired"]) { - if (normalizedErrors["https://json-schema.org/keyword/dependentRequired"][schemaLocation]) { + const dependentRequiredErrors = normalizedErrors["https://json-schema.org/keyword/dependentRequired"]; + if (!dependentRequiredErrors) { + return []; + } + + for (const schemaLocation in dependentRequiredErrors) { + if (dependentRequiredErrors[schemaLocation]) { continue; } @@ -31,11 +44,13 @@ const dependentRequiredErrorHandler = async (normalizedErrors, instance, localiz } } - errors.push({ - message: localization.getRequiredErrorMessage([...required]), - instanceLocation: Instance.uri(instance), - schemaLocations: [schemaLocation] - }); + if (required.size > 0) { + errors.push({ + message: localization.getRequiredErrorMessage([...required].sort()), + instanceLocation: Instance.uri(instance), + schemaLocations: [schemaLocation] + }); + } } return errors; diff --git a/src/error-handlers/required.js b/src/error-handlers/required.js index b9677a2..413ac72 100644 --- a/src/error-handlers/required.js +++ b/src/error-handlers/required.js @@ -8,32 +8,66 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; /** @type ErrorHandler */ const requiredErrorHandler = async (normalizedErrors, instance, localization) => { - /** @type ErrorObject[] */ - const errors = []; + const allMissingRequired = new Set(); + const allSchemaLocations = new Set(); + let hasRequiredFailure = false; - for (const schemaLocation in normalizedErrors["https://json-schema.org/keyword/required"]) { - if (normalizedErrors["https://json-schema.org/keyword/required"][schemaLocation]) { - continue; + const requiredErrors = normalizedErrors["https://json-schema.org/keyword/required"]; + if (requiredErrors) { + for (const schemaLocation in requiredErrors) { + if (requiredErrors[schemaLocation] === false) { + hasRequiredFailure = true; + allSchemaLocations.add(schemaLocation); + const keyword = await getSchema(schemaLocation); + const required = /** @type string[] */ (Schema.value(keyword)); + + for (const propertyName of required) { + if (!Instance.has(propertyName, instance)) { + allMissingRequired.add(propertyName); + } + } + } } + } - const keyword = await getSchema(schemaLocation); - const required = /** @type string[] */ (Schema.value(keyword)); + if (hasRequiredFailure) { + const dependentRequiredErrors = normalizedErrors["https://json-schema.org/keyword/dependentRequired"]; + if (dependentRequiredErrors) { + for (const schemaLocation in dependentRequiredErrors) { + if (dependentRequiredErrors[schemaLocation] === false) { + allSchemaLocations.add(schemaLocation); + const keyword = await getSchema(schemaLocation); + const dependentRequired = /** @type Record */ (Schema.value(keyword)); - const missingRequired = []; - for (const propertyName of required) { - if (!Instance.has(propertyName, instance)) { - missingRequired.push(propertyName); + for (const propertyName in dependentRequired) { + if (Instance.has(propertyName, instance)) { + for (const requiredPropertyName of dependentRequired[propertyName]) { + if (!Instance.has(requiredPropertyName, instance)) { + allMissingRequired.add(requiredPropertyName); + } + } + } + } + } } } + } + + if (allMissingRequired.size > 0) { + /** @type {string[]} */ + const missingProperties = [...allMissingRequired].sort(); + + /** @type {string[]} */ + const locations = [...allSchemaLocations].sort(); - errors.push({ - message: localization.getRequiredErrorMessage(missingRequired), + return [{ + message: localization.getRequiredErrorMessage(missingProperties), instanceLocation: Instance.uri(instance), - schemaLocations: [schemaLocation] - }); + schemaLocations: locations + }]; } - return errors; + return []; }; export default requiredErrorHandler; diff --git a/src/test-suite/tests/dependencies.json b/src/test-suite/tests/dependencies.json index d9bd39a..855df7a 100644 --- a/src/test-suite/tests/dependencies.json +++ b/src/test-suite/tests/dependencies.json @@ -110,20 +110,11 @@ { "messageId": "required-message", "messageParams": { - "count": 1, - "required": { "and": ["baz"] } - }, - "instanceLocation": "#", - "schemaLocations": ["#/dependentSchemas/foo/required"] - }, - { - "messageId": "required-message", - "messageParams": { - "count": 1, - "required": { "and": ["bbb"] } + "count": 2, + "required": { "and": ["baz", "bbb"] } }, "instanceLocation": "#", - "schemaLocations": ["#/dependentSchemas/aaa/required"] + "schemaLocations": ["#/dependentSchemas/aaa/required", "#/dependentSchemas/foo/required"] } ] }, diff --git a/src/test-suite/tests/required.json b/src/test-suite/tests/required.json index 6ac31e6..0c3db89 100644 --- a/src/test-suite/tests/required.json +++ b/src/test-suite/tests/required.json @@ -14,13 +14,35 @@ "messageId": "required-message", "messageParams": { "count": 1, - "required": { "or": ["bar"] } + "required": { "and": ["bar"] } }, "instanceLocation": "#", "schemaLocations": ["#/required"] } ] }, + { + "description": "required and dependentRequired", + "compatibility": ">=2019", + "schema": { + "required": ["foo", "bar"], + "dependentRequired": { + "a": ["b"] + } + }, + "instance": { "a": 1 }, + "errors": [ + { + "messageId": "required-message", + "messageParams": { + "count": 3, + "required": { "and": ["b", "bar", "foo"] } + }, + "instanceLocation": "#", + "schemaLocations": ["#/dependentRequired", "#/required"] + } + ] + }, { "description": "required pass", "schema": { From b2b2771770c59ad385f2339db205c9b1eae9d0dc Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 4 Feb 2026 15:39:03 +0530 Subject: [PATCH 2/2] unified error handler for each keyword --- src/error-handlers/dependentRequired.js | 59 ------ src/error-handlers/draft-04/dependencies.js | 22 --- src/error-handlers/required.js | 53 ++++-- src/index.js | 2 - .../draft-04/dependencies.js | 12 +- src/test-suite/tests/dependencies.json | 175 +++++++++++++----- src/test-suite/tests/required.json | 2 +- 7 files changed, 180 insertions(+), 145 deletions(-) delete mode 100644 src/error-handlers/dependentRequired.js diff --git a/src/error-handlers/dependentRequired.js b/src/error-handlers/dependentRequired.js deleted file mode 100644 index f15fdf7..0000000 --- a/src/error-handlers/dependentRequired.js +++ /dev/null @@ -1,59 +0,0 @@ -import { getSchema } from "@hyperjump/json-schema/experimental"; -import * as Schema from "@hyperjump/browser"; -import * as Instance from "@hyperjump/json-schema/instance/experimental"; - -/** - * @import { ErrorHandler, ErrorObject } from "../index.d.ts" - */ - -/** @type ErrorHandler */ -const dependentRequiredErrorHandler = async (normalizedErrors, instance, localization) => { - if (normalizedErrors["https://json-schema.org/keyword/required"]) { - for (const schemaLocation in normalizedErrors["https://json-schema.org/keyword/required"]) { - if (normalizedErrors["https://json-schema.org/keyword/required"][schemaLocation] === false) { - return []; - } - } - } - - /** @type ErrorObject[] */ - const errors = []; - - const dependentRequiredErrors = normalizedErrors["https://json-schema.org/keyword/dependentRequired"]; - if (!dependentRequiredErrors) { - return []; - } - - for (const schemaLocation in dependentRequiredErrors) { - if (dependentRequiredErrors[schemaLocation]) { - continue; - } - - const keyword = await getSchema(schemaLocation); - const dependentRequired = /** @type Record */ (Schema.value(keyword)); - - /** @type Set */ - const required = new Set(); - for (const propertyName in dependentRequired) { - if (Instance.has(propertyName, instance)) { - for (const requiredPropertyName of dependentRequired[propertyName]) { - if (!Instance.has(requiredPropertyName, instance)) { - required.add(requiredPropertyName); - } - } - } - } - - if (required.size > 0) { - errors.push({ - message: localization.getRequiredErrorMessage([...required].sort()), - instanceLocation: Instance.uri(instance), - schemaLocations: [schemaLocation] - }); - } - } - - return errors; -}; - -export default dependentRequiredErrorHandler; diff --git a/src/error-handlers/draft-04/dependencies.js b/src/error-handlers/draft-04/dependencies.js index d16effc..c01a9f5 100644 --- a/src/error-handlers/draft-04/dependencies.js +++ b/src/error-handlers/draft-04/dependencies.js @@ -1,6 +1,3 @@ -import { getSchema } from "@hyperjump/json-schema/experimental"; -import * as Schema from "@hyperjump/browser"; -import * as Instance from "@hyperjump/json-schema/instance/experimental"; import { getErrors } from "../../json-schema-errors.js"; /** @@ -22,25 +19,6 @@ const dependenciesErrorHandler = async (normalizedErrors, instance, localization const dependentSchemaErrors = await getErrors(dependentSchemaOutput, instance, localization); errors.push(...dependentSchemaErrors); } - - const dependencies = await getSchema(schemaLocation); - for await (const [propertyName, dependency] of Schema.entries(dependencies)) { - if (!Instance.has(propertyName, instance)) { - continue; - } - - if (Schema.typeOf(dependency) !== "array") { - continue; - } - - const dependentRequired = /** @type {string[]} */ (Schema.value(dependency)); - const missing = dependentRequired.filter((required) => !Instance.has(required, instance)); - errors.push({ - message: localization.getRequiredErrorMessage(missing), - instanceLocation: Instance.uri(instance), - schemaLocations: [schemaLocation] - }); - } } return errors; diff --git a/src/error-handlers/required.js b/src/error-handlers/required.js index 413ac72..b06f88c 100644 --- a/src/error-handlers/required.js +++ b/src/error-handlers/required.js @@ -10,13 +10,11 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; const requiredErrorHandler = async (normalizedErrors, instance, localization) => { const allMissingRequired = new Set(); const allSchemaLocations = new Set(); - let hasRequiredFailure = false; const requiredErrors = normalizedErrors["https://json-schema.org/keyword/required"]; if (requiredErrors) { for (const schemaLocation in requiredErrors) { if (requiredErrors[schemaLocation] === false) { - hasRequiredFailure = true; allSchemaLocations.add(schemaLocation); const keyword = await getSchema(schemaLocation); const required = /** @type string[] */ (Schema.value(keyword)); @@ -30,18 +28,43 @@ const requiredErrorHandler = async (normalizedErrors, instance, localization) => } } - if (hasRequiredFailure) { - const dependentRequiredErrors = normalizedErrors["https://json-schema.org/keyword/dependentRequired"]; - if (dependentRequiredErrors) { - for (const schemaLocation in dependentRequiredErrors) { - if (dependentRequiredErrors[schemaLocation] === false) { - allSchemaLocations.add(schemaLocation); - const keyword = await getSchema(schemaLocation); - const dependentRequired = /** @type Record */ (Schema.value(keyword)); + const dependentRequiredErrors = normalizedErrors["https://json-schema.org/keyword/dependentRequired"]; + if (dependentRequiredErrors) { + for (const schemaLocation in dependentRequiredErrors) { + if (dependentRequiredErrors[schemaLocation] === false) { + allSchemaLocations.add(schemaLocation); + const keyword = await getSchema(schemaLocation); + const dependentRequired = /** @type Record */ (Schema.value(keyword)); + + for (const propertyName in dependentRequired) { + if (Instance.has(propertyName, instance)) { + for (const requiredPropertyName of dependentRequired[propertyName]) { + if (!Instance.has(requiredPropertyName, instance)) { + allMissingRequired.add(requiredPropertyName); + } + } + } + } + } + } + } + + const dependenciesErrors = normalizedErrors["https://json-schema.org/keyword/draft-04/dependencies"]; + if (dependenciesErrors) { + for (const schemaLocation in dependenciesErrors) { + const dependencyValue = dependenciesErrors[schemaLocation]; + if (dependencyValue === false || Array.isArray(dependencyValue)) { + const keyword = await getSchema(schemaLocation); + const dependencies = /** @type Record */ (Schema.value(keyword)); - for (const propertyName in dependentRequired) { - if (Instance.has(propertyName, instance)) { - for (const requiredPropertyName of dependentRequired[propertyName]) { + let hasArrayFormDependencies = false; + for (const propertyName in dependencies) { + if (Instance.has(propertyName, instance)) { + const dependency = dependencies[propertyName]; + if (Array.isArray(dependency)) { + hasArrayFormDependencies = true; + const dependencyArray = /** @type {string[]} */ (dependency); + for (const requiredPropertyName of dependencyArray) { if (!Instance.has(requiredPropertyName, instance)) { allMissingRequired.add(requiredPropertyName); } @@ -49,6 +72,10 @@ const requiredErrorHandler = async (normalizedErrors, instance, localization) => } } } + + if (hasArrayFormDependencies) { + allSchemaLocations.add(schemaLocation); + } } } } diff --git a/src/index.js b/src/index.js index 1bff19e..ce48b7f 100644 --- a/src/index.js +++ b/src/index.js @@ -58,7 +58,6 @@ import booleanSchemaErrorHandler from "./error-handlers/boolean-schema.js"; import constErrorHandler from "./error-handlers/const.js"; import containsErrorHandler from "./error-handlers/contains.js"; import dependenciesErrorHandler from "./error-handlers/draft-04/dependencies.js"; -import dependentRequiredErrorHandler from "./error-handlers/dependentRequired.js"; import enumErrorHandler from "./error-handlers/enum.js"; import exclusiveMaximumErrorHandler from "./error-handlers/exclusiveMaximum.js"; import exclusiveMinimumErrorHandler from "./error-handlers/exclusiveMinimum.js"; @@ -144,7 +143,6 @@ addErrorHandler(booleanSchemaErrorHandler); addErrorHandler(constErrorHandler); addErrorHandler(containsErrorHandler); addErrorHandler(dependenciesErrorHandler); -addErrorHandler(dependentRequiredErrorHandler); addErrorHandler(enumErrorHandler); addErrorHandler(exclusiveMaximumErrorHandler); addErrorHandler(exclusiveMinimumErrorHandler); diff --git a/src/normalization-handlers/draft-04/dependencies.js b/src/normalization-handlers/draft-04/dependencies.js index e2cf5c9..b4de39f 100644 --- a/src/normalization-handlers/draft-04/dependencies.js +++ b/src/normalization-handlers/draft-04/dependencies.js @@ -12,18 +12,18 @@ const dependenciesNormalizationHandler = { const outputs = []; if (Instance.typeOf(instance) !== "object") { - return outputs; + return undefined; } for (const [propertyName, dependency] of dependencies) { - if (!Instance.has(propertyName, instance) || typeof dependency !== "string") { + if (!Instance.has(propertyName, instance)) { continue; } - - outputs.push(evaluateSchema(dependency, instance, context)); + if (typeof dependency === "string") { + outputs.push(evaluateSchema(dependency, instance, context)); + } } - - return outputs; + return outputs.length > 0 ? outputs : undefined; } }; diff --git a/src/test-suite/tests/dependencies.json b/src/test-suite/tests/dependencies.json index 855df7a..20b57f2 100644 --- a/src/test-suite/tests/dependencies.json +++ b/src/test-suite/tests/dependencies.json @@ -1,6 +1,5 @@ { "$schema": "../test-suite.schema.json", - "description": "The dependentRequired, dependentSchemas, and dependencies keywords", "tests": [ { @@ -8,8 +7,13 @@ "compatibility": "2019", "schema": { "dependentRequired": { - "foo": ["bar", "baz"], - "aaa": ["bbb"] + "foo": [ + "bar", + "baz" + ], + "aaa": [ + "bbb" + ] } }, "instance": { @@ -22,10 +26,17 @@ "messageId": "required-message", "messageParams": { "count": 2, - "required": { "and": ["baz", "bbb"] } + "required": { + "and": [ + "baz", + "bbb" + ] + } }, "instanceLocation": "#", - "schemaLocations": ["#/dependentRequired"] + "schemaLocations": [ + "#/dependentRequired" + ] } ] }, @@ -34,8 +45,13 @@ "compatibility": "<=7", "schema": { "dependencies": { - "foo": ["bar", "baz"], - "aaa": ["bbb"] + "foo": [ + "bar", + "baz" + ], + "aaa": [ + "bbb" + ] } }, "instance": { @@ -47,20 +63,18 @@ { "messageId": "required-message", "messageParams": { - "count": 1, - "required": { "and": ["baz"] } - }, - "instanceLocation": "#", - "schemaLocations": ["#/dependencies"] - }, - { - "messageId": "required-message", - "messageParams": { - "count": 1, - "required": { "and": ["bbb"] } + "count": 2, + "required": { + "and": [ + "baz", + "bbb" + ] + } }, "instanceLocation": "#", - "schemaLocations": ["#/dependencies"] + "schemaLocations": [ + "#/dependencies" + ] } ] }, @@ -69,7 +83,9 @@ "compatibility": "2019", "schema": { "dependentRequired": { - "foo": ["bar"] + "foo": [ + "bar" + ] } }, "instance": { @@ -83,7 +99,9 @@ "compatibility": "<=7", "schema": { "dependencies": { - "foo": ["bar"] + "foo": [ + "bar" + ] } }, "instance": { @@ -97,8 +115,17 @@ "compatibility": "2019", "schema": { "dependentSchemas": { - "foo": { "required": ["bar", "baz"] }, - "aaa": { "required": ["bbb"] } + "foo": { + "required": [ + "bar", + "baz" + ] + }, + "aaa": { + "required": [ + "bbb" + ] + } } }, "instance": { @@ -111,10 +138,18 @@ "messageId": "required-message", "messageParams": { "count": 2, - "required": { "and": ["baz", "bbb"] } + "required": { + "and": [ + "baz", + "bbb" + ] + } }, "instanceLocation": "#", - "schemaLocations": ["#/dependentSchemas/aaa/required", "#/dependentSchemas/foo/required"] + "schemaLocations": [ + "#/dependentSchemas/aaa/required", + "#/dependentSchemas/foo/required" + ] } ] }, @@ -123,8 +158,17 @@ "compatibility": "<=7", "schema": { "dependencies": { - "foo": { "required": ["bar", "baz"] }, - "aaa": { "required": ["bbb"] } + "foo": { + "required": [ + "bar", + "baz" + ] + }, + "aaa": { + "required": [ + "bbb" + ] + } } }, "instance": { @@ -137,19 +181,31 @@ "messageId": "required-message", "messageParams": { "count": 1, - "required": { "and": ["baz"] } + "required": { + "and": [ + "baz" + ] + } }, "instanceLocation": "#", - "schemaLocations": ["#/dependencies/foo/required"] + "schemaLocations": [ + "#/dependencies/foo/required" + ] }, { "messageId": "required-message", "messageParams": { "count": 1, - "required": { "and": ["bbb"] } + "required": { + "and": [ + "bbb" + ] + } }, "instanceLocation": "#", - "schemaLocations": ["#/dependencies/aaa/required"] + "schemaLocations": [ + "#/dependencies/aaa/required" + ] } ] }, @@ -158,7 +214,11 @@ "compatibility": "2019", "schema": { "dependentSchemas": { - "foo": { "required": ["bar"] } + "foo": { + "required": [ + "bar" + ] + } } }, "instance": 42, @@ -169,7 +229,11 @@ "compatibility": "<=7", "schema": { "dependentSchemas": { - "foo": { "required": ["bar"] } + "foo": { + "required": [ + "bar" + ] + } } }, "instance": 42, @@ -179,7 +243,11 @@ "description": "dependentSchemas pass", "schema": { "dependentSchemas": { - "foo": { "required": ["bar"] } + "foo": { + "required": [ + "bar" + ] + } } }, "instance": { @@ -192,7 +260,11 @@ "description": "schem-form dependencies pass", "schema": { "dependencies": { - "foo": { "required": ["bar"] } + "foo": { + "required": [ + "bar" + ] + } } }, "instance": { @@ -206,8 +278,15 @@ "compatibility": "<=7", "schema": { "dependencies": { - "foo": { "required": ["bar", "baz"] }, - "aaa": ["bbb"] + "foo": { + "required": [ + "bar", + "baz" + ] + }, + "aaa": [ + "bbb" + ] } }, "instance": { @@ -220,21 +299,33 @@ "messageId": "required-message", "messageParams": { "count": 1, - "required": { "and": ["baz"] } + "required": { + "and": [ + "baz" + ] + } }, "instanceLocation": "#", - "schemaLocations": ["#/dependencies/foo/required"] + "schemaLocations": [ + "#/dependencies/foo/required" + ] }, { "messageId": "required-message", "messageParams": { "count": 1, - "required": { "and": ["bbb"] } + "required": { + "and": [ + "bbb" + ] + } }, "instanceLocation": "#", - "schemaLocations": ["#/dependencies"] + "schemaLocations": [ + "#/dependencies" + ] } ] } ] -} +} \ No newline at end of file diff --git a/src/test-suite/tests/required.json b/src/test-suite/tests/required.json index 0c3db89..1aab783 100644 --- a/src/test-suite/tests/required.json +++ b/src/test-suite/tests/required.json @@ -22,7 +22,7 @@ ] }, { - "description": "required and dependentRequired", + "description": "combined required and dependentRequired", "compatibility": ">=2019", "schema": { "required": ["foo", "bar"],