From 3aa922c8e9919c294bf6cdb3a376017763d6e460 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 18 Feb 2026 13:45:44 +0530 Subject: [PATCH 1/6] combined const/enum with type error messages --- src/error-handlers/constEnum.js | 73 +++++++++++++++++++++++++++++++++ src/error-handlers/type.js | 8 ++++ src/test-suite/tests/const.json | 22 ++++++++++ src/test-suite/tests/enum.json | 20 +++++++++ 4 files changed, 123 insertions(+) diff --git a/src/error-handlers/constEnum.js b/src/error-handlers/constEnum.js index ab0a311..c472a48 100644 --- a/src/error-handlers/constEnum.js +++ b/src/error-handlers/constEnum.js @@ -14,6 +14,8 @@ import jsonStringify from "json-stringify-deterministic"; * }} Constraint */ +const ALL_TYPES = new Set(["null", "boolean", "number", "string", "array", "object", "integer"]); + /** @type {ErrorHandler} */ const constEnumErrorHandler = async (normalizedErrors, instance, localization) => { /** @type Set | undefined */ @@ -52,6 +54,77 @@ const constEnumErrorHandler = async (normalizedErrors, instance, localization) = allowedJson = allowedJson?.intersection(keywordJson) ?? keywordJson; } + if (allSchemaLocations.length === 0) { + return []; + } + + if (normalizedErrors["https://json-schema.org/keyword/type"] && allowedJson) { + let allowedTypes = ALL_TYPES; + /** @type {string[]} */ + const typeLocations = []; + + for (const schemaLocation in normalizedErrors["https://json-schema.org/keyword/type"]) { + typeLocations.push(schemaLocation); + const keyword = await getSchema(schemaLocation); + /** @type {string | string[]} */ + const value = Schema.value(keyword); + const types = Array.isArray(value) ? value : [value]; + /** @type {Set} */ + const keywordTypes = new Set(types); + if (keywordTypes.has("number")) { + keywordTypes.add("integer"); + } + allowedTypes = allowedTypes.intersection(keywordTypes); + } + + if (allowedTypes.has("number")) { + allowedTypes.delete("integer"); + } + + /** @type {Set} */ + const filteredJson = new Set(); + for (const jsonStr of allowedJson) { + /** @type {Json} */ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const val = JSON.parse(jsonStr); + let valueType = val === null ? "null" : Array.isArray(val) ? "array" : typeof val; + if (valueType === "object") { + valueType = "object"; + } + if (valueType === "number" && Number.isInteger(val)) { + valueType = "integer"; + } + + if (allowedTypes.has(valueType) + || (valueType === "integer" && allowedTypes.has("number"))) { + filteredJson.add(jsonStr); + } + } + + if (filteredJson.size === 0) { + if (constSchemaLocations.length === 0 && enumSchemaLocations.length === 0) { + constSchemaLocations.push(...allSchemaLocations.filter((loc) => + !typeLocations.includes(loc))); + enumSchemaLocations.push(...constSchemaLocations); + } + const constEnumLocations = allSchemaLocations.filter((loc) => + !typeLocations.includes(loc)); + allSchemaLocations.length = 0; + allSchemaLocations.push(...typeLocations, ...constEnumLocations); + } else { + const instanceJson = jsonStringify(Instance.value(instance)); + if (!filteredJson.has(instanceJson)) { + if (constSchemaLocations.length === 0 && enumSchemaLocations.length === 0) { + constSchemaLocations.push(...allSchemaLocations.filter((loc) => + !typeLocations.includes(loc))); + enumSchemaLocations.push(...constSchemaLocations); + } + } + } + + allowedJson = filteredJson; + } + if (constSchemaLocations.length === 0 && enumSchemaLocations.length === 0) { return []; } diff --git a/src/error-handlers/type.js b/src/error-handlers/type.js index 99bcb9b..e4a931f 100644 --- a/src/error-handlers/type.js +++ b/src/error-handlers/type.js @@ -14,6 +14,14 @@ const typeErrorHandler = async (normalizedErrors, instance, localization) => { const errors = []; if (normalizedErrors["https://json-schema.org/keyword/type"]) { + const hasConstOrEnum + = normalizedErrors["https://json-schema.org/keyword/const"] + || normalizedErrors["https://json-schema.org/keyword/enum"]; + + if (hasConstOrEnum) { + return errors; + } + /** @type {Set} */ let allowedTypes = ALL_TYPES; const failedTypeLocations = []; diff --git a/src/test-suite/tests/const.json b/src/test-suite/tests/const.json index 559ca3a..b88061e 100644 --- a/src/test-suite/tests/const.json +++ b/src/test-suite/tests/const.json @@ -92,6 +92,28 @@ ] } ] + }, + { + "description": "const with type - filter by type", + "compatibility": "6", + "schema": { + "allOf": [ + { "type": "number" }, + { "const": "foo" } + ] + }, + "instance": "foo", + "errors": [ + { + "messageId": "boolean-schema-message", + "messageParams": {}, + "instanceLocation": "#", + "schemaLocations": [ + "#/allOf/0/type", + "#/allOf/1/const" + ] + } + ] } ] } diff --git a/src/test-suite/tests/enum.json b/src/test-suite/tests/enum.json index 86be502..c91ac05 100644 --- a/src/test-suite/tests/enum.json +++ b/src/test-suite/tests/enum.json @@ -99,6 +99,26 @@ ] } ] + }, + { + "description": "enum with type - filter by type", + "schema": { + "allOf": [ + { "type": "string" }, + { "enum": ["foo", 42] } + ] + }, + "instance": 42, + "errors": [ + { + "messageId": "const-message", + "messageParams": { + "expected": "\"foo\"" + }, + "instanceLocation": "#", + "schemaLocations": ["#/allOf/1/enum"] + } + ] } ] } From 330969be3f79b07b91b02182fa232a097a23639a Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 20 Feb 2026 18:09:55 +0530 Subject: [PATCH 2/6] type and constEnum handlers are combined --- src/error-handlers/type.js | 69 ----------- .../{constEnum.js => typeConstEnum.js} | 115 ++++++++++++------ src/index.js | 6 +- 3 files changed, 78 insertions(+), 112 deletions(-) delete mode 100644 src/error-handlers/type.js rename src/error-handlers/{constEnum.js => typeConstEnum.js} (65%) diff --git a/src/error-handlers/type.js b/src/error-handlers/type.js deleted file mode 100644 index e4a931f..0000000 --- a/src/error-handlers/type.js +++ /dev/null @@ -1,69 +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" - */ - -const ALL_TYPES = new Set(["null", "boolean", "number", "string", "array", "object", "integer"]); - -/** @type ErrorHandler */ -const typeErrorHandler = async (normalizedErrors, instance, localization) => { - /** @type ErrorObject[] */ - const errors = []; - - if (normalizedErrors["https://json-schema.org/keyword/type"]) { - const hasConstOrEnum - = normalizedErrors["https://json-schema.org/keyword/const"] - || normalizedErrors["https://json-schema.org/keyword/enum"]; - - if (hasConstOrEnum) { - return errors; - } - - /** @type {Set} */ - let allowedTypes = ALL_TYPES; - const failedTypeLocations = []; - - for (const schemaLocation in normalizedErrors["https://json-schema.org/keyword/type"]) { - const isValid = normalizedErrors["https://json-schema.org/keyword/type"][schemaLocation]; - if (!isValid) { - failedTypeLocations.push(schemaLocation); - - const keyword = await getSchema(schemaLocation); - /** @type {string|string[]} */ - const value = Schema.value(keyword); - const types = Array.isArray(value) ? value : [value]; - /** @type {Set} */ - const keywordTypes = new Set(types); - if (keywordTypes.has("number")) { - keywordTypes.add("integer"); - } - allowedTypes = allowedTypes.intersection(keywordTypes); - } - } - - if (allowedTypes.has("number")) { - allowedTypes.delete("integer"); - } - - if (allowedTypes.size === 0) { - errors.push({ - message: localization.getBooleanSchemaErrorMessage(), - instanceLocation: Instance.uri(instance), - schemaLocations: failedTypeLocations - }); - } else if (failedTypeLocations.length > 0) { - errors.push({ - message: localization.getTypeErrorMessage([...allowedTypes]), - instanceLocation: Instance.uri(instance), - schemaLocations: failedTypeLocations - }); - } - } - - return errors; -}; - -export default typeErrorHandler; diff --git a/src/error-handlers/constEnum.js b/src/error-handlers/typeConstEnum.js similarity index 65% rename from src/error-handlers/constEnum.js rename to src/error-handlers/typeConstEnum.js index c472a48..5080910 100644 --- a/src/error-handlers/constEnum.js +++ b/src/error-handlers/typeConstEnum.js @@ -4,7 +4,7 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; import jsonStringify from "json-stringify-deterministic"; /** - * @import { ErrorHandler, Json } from "../index.d.ts" + * @import { ErrorHandler, InstanceOutput, Json } from "../index.d.ts" */ /** @@ -17,17 +17,45 @@ import jsonStringify from "json-stringify-deterministic"; const ALL_TYPES = new Set(["null", "boolean", "number", "string", "array", "object", "integer"]); /** @type {ErrorHandler} */ -const constEnumErrorHandler = async (normalizedErrors, instance, localization) => { - /** @type Set | undefined */ +const typeConstEnumErrorHandler = async (normalizedErrors, instance, localization) => { + const hasType = !!normalizedErrors["https://json-schema.org/keyword/type"]; + const hasConst = !!normalizedErrors["https://json-schema.org/keyword/const"]; + const hasEnum = !!normalizedErrors["https://json-schema.org/keyword/enum"]; + + if (!hasType && !hasConst && !hasEnum) { + return []; + } + + const { allowedTypes, failedTypeLocations } = hasType + ? await resolveTypes(normalizedErrors) + : { allowedTypes: ALL_TYPES, failedTypeLocations: [] }; + + if (!hasConst && !hasEnum) { + if (allowedTypes.size === 0) { + return [{ + message: localization.getBooleanSchemaErrorMessage(), + instanceLocation: Instance.uri(instance), + schemaLocations: failedTypeLocations + }]; + } else if (failedTypeLocations.length > 0) { + return [{ + message: localization.getTypeErrorMessage([...allowedTypes]), + instanceLocation: Instance.uri(instance), + schemaLocations: failedTypeLocations + }]; + } + + return []; + } + + /** @type {Set | undefined} */ let allowedJson; - /** @type string[]> */ + /** @type {string[]} */ const constSchemaLocations = []; - - /** @type string[]> */ + /** @type {string[]} */ const enumSchemaLocations = []; - - /** @type string[]> */ + /** @type {string[]} */ const allSchemaLocations = []; for (const schemaLocation in normalizedErrors["https://json-schema.org/keyword/const"]) { @@ -58,29 +86,7 @@ const constEnumErrorHandler = async (normalizedErrors, instance, localization) = return []; } - if (normalizedErrors["https://json-schema.org/keyword/type"] && allowedJson) { - let allowedTypes = ALL_TYPES; - /** @type {string[]} */ - const typeLocations = []; - - for (const schemaLocation in normalizedErrors["https://json-schema.org/keyword/type"]) { - typeLocations.push(schemaLocation); - const keyword = await getSchema(schemaLocation); - /** @type {string | string[]} */ - const value = Schema.value(keyword); - const types = Array.isArray(value) ? value : [value]; - /** @type {Set} */ - const keywordTypes = new Set(types); - if (keywordTypes.has("number")) { - keywordTypes.add("integer"); - } - allowedTypes = allowedTypes.intersection(keywordTypes); - } - - if (allowedTypes.has("number")) { - allowedTypes.delete("integer"); - } - + if (hasType && allowedJson) { /** @type {Set} */ const filteredJson = new Set(); for (const jsonStr of allowedJson) { @@ -88,9 +94,6 @@ const constEnumErrorHandler = async (normalizedErrors, instance, localization) = // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const val = JSON.parse(jsonStr); let valueType = val === null ? "null" : Array.isArray(val) ? "array" : typeof val; - if (valueType === "object") { - valueType = "object"; - } if (valueType === "number" && Number.isInteger(val)) { valueType = "integer"; } @@ -104,19 +107,19 @@ const constEnumErrorHandler = async (normalizedErrors, instance, localization) = if (filteredJson.size === 0) { if (constSchemaLocations.length === 0 && enumSchemaLocations.length === 0) { constSchemaLocations.push(...allSchemaLocations.filter((loc) => - !typeLocations.includes(loc))); + !failedTypeLocations.includes(loc))); enumSchemaLocations.push(...constSchemaLocations); } const constEnumLocations = allSchemaLocations.filter((loc) => - !typeLocations.includes(loc)); + !failedTypeLocations.includes(loc)); allSchemaLocations.length = 0; - allSchemaLocations.push(...typeLocations, ...constEnumLocations); + allSchemaLocations.push(...failedTypeLocations, ...constEnumLocations); } else { const instanceJson = jsonStringify(Instance.value(instance)); if (!filteredJson.has(instanceJson)) { if (constSchemaLocations.length === 0 && enumSchemaLocations.length === 0) { constSchemaLocations.push(...allSchemaLocations.filter((loc) => - !typeLocations.includes(loc))); + !failedTypeLocations.includes(loc))); enumSchemaLocations.push(...constSchemaLocations); } } @@ -148,4 +151,38 @@ const constEnumErrorHandler = async (normalizedErrors, instance, localization) = } }; -export default constEnumErrorHandler; +/** + * @param {InstanceOutput} normalizedErrors + * @returns {Promise<{ allowedTypes: Set; failedTypeLocations: string[] }>} + */ +async function resolveTypes(normalizedErrors) { + let allowedTypes = ALL_TYPES; + /** @type {string[]} */ + const failedTypeLocations = []; + + for (const schemaLocation in normalizedErrors["https://json-schema.org/keyword/type"]) { + const isValid = normalizedErrors["https://json-schema.org/keyword/type"][schemaLocation]; + if (!isValid) { + failedTypeLocations.push(schemaLocation); + + const keyword = await getSchema(schemaLocation); + /** @type {string | string[]} */ + const value = Schema.value(keyword); + const types = Array.isArray(value) ? value : [value]; + /** @type {Set} */ + const keywordTypes = new Set(types); + if (keywordTypes.has("number")) { + keywordTypes.add("integer"); + } + allowedTypes = allowedTypes.intersection(keywordTypes); + } + } + + if (allowedTypes.has("number")) { + allowedTypes.delete("integer"); + } + + return { allowedTypes, failedTypeLocations }; +} + +export default typeConstEnumErrorHandler; diff --git a/src/index.js b/src/index.js index c617f46..65c90ae 100644 --- a/src/index.js +++ b/src/index.js @@ -55,7 +55,6 @@ import unknownNormalizationHandler from "./normalization-handlers/unknown.js"; // Error Handlers import anyOfErrorHandler from "./error-handlers/anyOf.js"; import booleanSchemaErrorHandler from "./error-handlers/boolean-schema.js"; -import constEnumErrorHandler from "./error-handlers/constEnum.js"; import containsErrorHandler from "./error-handlers/contains.js"; import dependenciesErrorHandler from "./error-handlers/draft-04/dependencies.js"; import exclusiveMaximumErrorHandler from "./error-handlers/exclusiveMaximum.js"; @@ -76,7 +75,7 @@ import notErrorHandler from "./error-handlers/not.js"; import oneOfErrorHandler from "./error-handlers/oneOf.js"; import patternErrorHandler from "./error-handlers/pattern.js"; import requiredErrorHandler from "./error-handlers/required.js"; -import typeErrorHandler from "./error-handlers/type.js"; +import typeConstEnumErrorHandler from "./error-handlers/typeConstEnum.js"; import uniqueItemsErrorHandler from "./error-handlers/uniqueItems.js"; import unknownErrorHandler from "./error-handlers/unknown.js"; @@ -139,7 +138,6 @@ setNormalizationHandler("https://json-schema.org/keyword/unknown", unknownNormal addErrorHandler(anyOfErrorHandler); addErrorHandler(booleanSchemaErrorHandler); -addErrorHandler(constEnumErrorHandler); addErrorHandler(containsErrorHandler); addErrorHandler(dependenciesErrorHandler); addErrorHandler(exclusiveMaximumErrorHandler); @@ -160,7 +158,7 @@ addErrorHandler(notErrorHandler); addErrorHandler(oneOfErrorHandler); addErrorHandler(patternErrorHandler); addErrorHandler(requiredErrorHandler); -addErrorHandler(typeErrorHandler); +addErrorHandler(typeConstEnumErrorHandler); addErrorHandler(uniqueItemsErrorHandler); addErrorHandler(unknownErrorHandler); From ac992bba710732e43cb0ca2847068de2c37b45a1 Mon Sep 17 00:00:00 2001 From: Sam Date: Sun, 22 Feb 2026 10:46:07 +0530 Subject: [PATCH 3/6] simplified the handler --- src/error-handlers/typeConstEnum.js | 79 ++++++++++------------------- src/test-suite/tests/const.json | 21 ++++++++ src/test-suite/tests/enum.json | 45 ++++++++++++++++ 3 files changed, 93 insertions(+), 52 deletions(-) diff --git a/src/error-handlers/typeConstEnum.js b/src/error-handlers/typeConstEnum.js index 5080910..df950de 100644 --- a/src/error-handlers/typeConstEnum.js +++ b/src/error-handlers/typeConstEnum.js @@ -7,13 +7,6 @@ import jsonStringify from "json-stringify-deterministic"; * @import { ErrorHandler, InstanceOutput, Json } from "../index.d.ts" */ -/** - * @typedef {{ - * allowedValues: Json[]; - * schemaLocation: string; - * }} Constraint - */ - const ALL_TYPES = new Set(["null", "boolean", "number", "string", "array", "object", "integer"]); /** @type {ErrorHandler} */ @@ -52,17 +45,17 @@ const typeConstEnumErrorHandler = async (normalizedErrors, instance, localizatio let allowedJson; /** @type {string[]} */ - const constSchemaLocations = []; + const constEnumLocations = []; /** @type {string[]} */ - const enumSchemaLocations = []; + const failedConstLocations = []; /** @type {string[]} */ - const allSchemaLocations = []; + const failedEnumLocations = []; for (const schemaLocation in normalizedErrors["https://json-schema.org/keyword/const"]) { + constEnumLocations.push(schemaLocation); if (!normalizedErrors["https://json-schema.org/keyword/const"][schemaLocation]) { - constSchemaLocations.push(schemaLocation); + failedConstLocations.push(schemaLocation); } - allSchemaLocations.push(schemaLocation); const keyword = await getSchema(schemaLocation); const keywordJson = new Set([jsonStringify(/** @type Json */ (Schema.value(keyword)))]); @@ -71,10 +64,10 @@ const typeConstEnumErrorHandler = async (normalizedErrors, instance, localizatio } for (const schemaLocation in normalizedErrors["https://json-schema.org/keyword/enum"]) { + constEnumLocations.push(schemaLocation); if (!normalizedErrors["https://json-schema.org/keyword/enum"][schemaLocation]) { - enumSchemaLocations.push(schemaLocation); + failedEnumLocations.push(schemaLocation); } - allSchemaLocations.push(schemaLocation); const keyword = await getSchema(schemaLocation); const keywordJson = new Set(/** @type Json[] */ (Schema.value(keyword)).map((value) => jsonStringify(value))); @@ -82,10 +75,7 @@ const typeConstEnumErrorHandler = async (normalizedErrors, instance, localizatio allowedJson = allowedJson?.intersection(keywordJson) ?? keywordJson; } - if (allSchemaLocations.length === 0) { - return []; - } - + let typeFiltered = false; if (hasType && allowedJson) { /** @type {Set} */ const filteredJson = new Set(); @@ -103,32 +93,15 @@ const typeConstEnumErrorHandler = async (normalizedErrors, instance, localizatio filteredJson.add(jsonStr); } } - - if (filteredJson.size === 0) { - if (constSchemaLocations.length === 0 && enumSchemaLocations.length === 0) { - constSchemaLocations.push(...allSchemaLocations.filter((loc) => - !failedTypeLocations.includes(loc))); - enumSchemaLocations.push(...constSchemaLocations); - } - const constEnumLocations = allSchemaLocations.filter((loc) => - !failedTypeLocations.includes(loc)); - allSchemaLocations.length = 0; - allSchemaLocations.push(...failedTypeLocations, ...constEnumLocations); - } else { - const instanceJson = jsonStringify(Instance.value(instance)); - if (!filteredJson.has(instanceJson)) { - if (constSchemaLocations.length === 0 && enumSchemaLocations.length === 0) { - constSchemaLocations.push(...allSchemaLocations.filter((loc) => - !failedTypeLocations.includes(loc))); - enumSchemaLocations.push(...constSchemaLocations); - } - } - } - + typeFiltered = filteredJson.size < allowedJson.size; allowedJson = filteredJson; } - if (constSchemaLocations.length === 0 && enumSchemaLocations.length === 0) { + const failedLocations = failedConstLocations.length > 0 + ? failedConstLocations + : failedEnumLocations; + + if (failedLocations.length === 0 && !typeFiltered) { return []; } @@ -136,19 +109,21 @@ const typeConstEnumErrorHandler = async (normalizedErrors, instance, localizatio return [{ message: localization.getBooleanSchemaErrorMessage(), instanceLocation: Instance.uri(instance), - schemaLocations: allSchemaLocations - }]; - } else { - /** @type Json[] */ - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - const allowedValues = [...allowedJson ?? []].map((json) => JSON.parse(json)); - - return [{ - message: localization.getEnumErrorMessage(allowedValues), - instanceLocation: Instance.uri(instance), - schemaLocations: constSchemaLocations.length ? constSchemaLocations : enumSchemaLocations + schemaLocations: [...failedTypeLocations, ...constEnumLocations] }]; } + + /** @type Json[] */ + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + const allowedValues = [...allowedJson ?? []].map((json) => JSON.parse(json)); + + return [{ + message: localization.getEnumErrorMessage(allowedValues), + instanceLocation: Instance.uri(instance), + schemaLocations: typeFiltered + ? [...failedTypeLocations, ...constEnumLocations] + : failedLocations + }]; }; /** diff --git a/src/test-suite/tests/const.json b/src/test-suite/tests/const.json index b88061e..8d11e64 100644 --- a/src/test-suite/tests/const.json +++ b/src/test-suite/tests/const.json @@ -114,6 +114,27 @@ ] } ] + }, + { + "description": "const with matching type", + "compatibility": "6", + "schema": { + "allOf": [ + { "type": "string" }, + { "const": "foo" } + ] + }, + "instance": 42, + "errors": [ + { + "messageId": "const-message", + "messageParams": { + "expected": "\"foo\"" + }, + "instanceLocation": "#", + "schemaLocations": ["#/allOf/1/const"] + } + ] } ] } diff --git a/src/test-suite/tests/enum.json b/src/test-suite/tests/enum.json index c91ac05..6f312e2 100644 --- a/src/test-suite/tests/enum.json +++ b/src/test-suite/tests/enum.json @@ -116,9 +116,54 @@ "expected": "\"foo\"" }, "instanceLocation": "#", + "schemaLocations": [ + "#/allOf/0/type", + "#/allOf/1/enum" + ] + } + ] + }, + { + "description": "enum with type - no filtering needed", + "schema": { + "allOf": [ + { "type": "string" }, + { "enum": ["foo", "bar"] } + ] + }, + "instance": 42, + "errors": [ + { + "messageId": "enum-message", + "messageParams": { + "expected": { "or": ["\"foo\"", "\"bar\""] }, + "count": 2 + }, + "instanceLocation": "#", "schemaLocations": ["#/allOf/1/enum"] } ] + }, + { + "description": "enum with type - all values filtered", + "schema": { + "allOf": [ + { "type": "boolean" }, + { "enum": ["foo", 42] } + ] + }, + "instance": "foo", + "errors": [ + { + "messageId": "boolean-schema-message", + "messageParams": {}, + "instanceLocation": "#", + "schemaLocations": [ + "#/allOf/0/type", + "#/allOf/1/enum" + ] + } + ] } ] } From 1d7b9842c940f9a240d43f5387a82c8a398f9df6 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 24 Feb 2026 07:47:10 +0530 Subject: [PATCH 4/6] refactored handler to filter during collection --- src/error-handlers/typeConstEnum.js | 54 ++++++++++++++++------------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/src/error-handlers/typeConstEnum.js b/src/error-handlers/typeConstEnum.js index df950de..d9e4ed9 100644 --- a/src/error-handlers/typeConstEnum.js +++ b/src/error-handlers/typeConstEnum.js @@ -24,6 +24,10 @@ const typeConstEnumErrorHandler = async (normalizedErrors, instance, localizatio : { allowedTypes: ALL_TYPES, failedTypeLocations: [] }; if (!hasConst && !hasEnum) { + if (allowedTypes.has("number")) { + allowedTypes.delete("integer"); + } + if (allowedTypes.size === 0) { return [{ message: localization.getBooleanSchemaErrorMessage(), @@ -50,6 +54,7 @@ const typeConstEnumErrorHandler = async (normalizedErrors, instance, localizatio const failedConstLocations = []; /** @type {string[]} */ const failedEnumLocations = []; + let typeFiltered = false; for (const schemaLocation in normalizedErrors["https://json-schema.org/keyword/const"]) { constEnumLocations.push(schemaLocation); @@ -58,7 +63,12 @@ const typeConstEnumErrorHandler = async (normalizedErrors, instance, localizatio } const keyword = await getSchema(schemaLocation); - const keywordJson = new Set([jsonStringify(/** @type Json */ (Schema.value(keyword)))]); + const keywordJson = new Set(); + if (allowedTypes.has(Schema.typeOf(keyword))) { + keywordJson.add(jsonStringify(Schema.value(keyword))); + } else { + typeFiltered = true; + } allowedJson = allowedJson?.intersection(keywordJson) ?? keywordJson; } @@ -70,31 +80,16 @@ const typeConstEnumErrorHandler = async (normalizedErrors, instance, localizatio } const keyword = await getSchema(schemaLocation); - const keywordJson = new Set(/** @type Json[] */ (Schema.value(keyword)).map((value) => jsonStringify(value))); - - allowedJson = allowedJson?.intersection(keywordJson) ?? keywordJson; - } - - let typeFiltered = false; - if (hasType && allowedJson) { - /** @type {Set} */ - const filteredJson = new Set(); - for (const jsonStr of allowedJson) { - /** @type {Json} */ - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const val = JSON.parse(jsonStr); - let valueType = val === null ? "null" : Array.isArray(val) ? "array" : typeof val; - if (valueType === "number" && Number.isInteger(val)) { - valueType = "integer"; - } - - if (allowedTypes.has(valueType) - || (valueType === "integer" && allowedTypes.has("number"))) { - filteredJson.add(jsonStr); + const keywordJson = new Set(); + for (const value of /** @type Json[] */ (Schema.value(keyword))) { + if (allowedTypes.has(jsonSchemaType(value))) { + keywordJson.add(jsonStringify(value)); + } else { + typeFiltered = true; } } - typeFiltered = filteredJson.size < allowedJson.size; - allowedJson = filteredJson; + + allowedJson = allowedJson?.intersection(keywordJson) ?? keywordJson; } const failedLocations = failedConstLocations.length > 0 @@ -160,4 +155,15 @@ async function resolveTypes(normalizedErrors) { return { allowedTypes, failedTypeLocations }; } +/** + * @param {Json} value + * @returns {string} + */ +function jsonSchemaType(value) { + if (value === null) return "null"; + if (Array.isArray(value)) return "array"; + if (typeof value === "number") return Number.isInteger(value) ? "integer" : "number"; + return typeof value; +} + export default typeConstEnumErrorHandler; From 039ba538166fbf9e4a11d950057310053942dcbc Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 25 Feb 2026 13:53:20 +0530 Subject: [PATCH 5/6] suggested cleanup --- src/error-handlers/typeConstEnum.js | 83 +++++++++++------------------ 1 file changed, 30 insertions(+), 53 deletions(-) diff --git a/src/error-handlers/typeConstEnum.js b/src/error-handlers/typeConstEnum.js index d9e4ed9..a27f17d 100644 --- a/src/error-handlers/typeConstEnum.js +++ b/src/error-handlers/typeConstEnum.js @@ -11,39 +11,7 @@ const ALL_TYPES = new Set(["null", "boolean", "number", "string", "array", "obje /** @type {ErrorHandler} */ const typeConstEnumErrorHandler = async (normalizedErrors, instance, localization) => { - const hasType = !!normalizedErrors["https://json-schema.org/keyword/type"]; - const hasConst = !!normalizedErrors["https://json-schema.org/keyword/const"]; - const hasEnum = !!normalizedErrors["https://json-schema.org/keyword/enum"]; - - if (!hasType && !hasConst && !hasEnum) { - return []; - } - - const { allowedTypes, failedTypeLocations } = hasType - ? await resolveTypes(normalizedErrors) - : { allowedTypes: ALL_TYPES, failedTypeLocations: [] }; - - if (!hasConst && !hasEnum) { - if (allowedTypes.has("number")) { - allowedTypes.delete("integer"); - } - - if (allowedTypes.size === 0) { - return [{ - message: localization.getBooleanSchemaErrorMessage(), - instanceLocation: Instance.uri(instance), - schemaLocations: failedTypeLocations - }]; - } else if (failedTypeLocations.length > 0) { - return [{ - message: localization.getTypeErrorMessage([...allowedTypes]), - instanceLocation: Instance.uri(instance), - schemaLocations: failedTypeLocations - }]; - } - - return []; - } + const { allowedTypes, failedTypeLocations } = await resolveTypes(normalizedErrors); /** @type {Set | undefined} */ let allowedJson; @@ -81,9 +49,9 @@ const typeConstEnumErrorHandler = async (normalizedErrors, instance, localizatio const keyword = await getSchema(schemaLocation); const keywordJson = new Set(); - for (const value of /** @type Json[] */ (Schema.value(keyword))) { - if (allowedTypes.has(jsonSchemaType(value))) { - keywordJson.add(jsonStringify(value)); + for await (const enumValueNode of Schema.iter(keyword)) { + if (allowedTypes.has(Schema.typeOf(enumValueNode))) { + keywordJson.add(jsonStringify(Schema.value(enumValueNode))); } else { typeFiltered = true; } @@ -96,10 +64,34 @@ const typeConstEnumErrorHandler = async (normalizedErrors, instance, localizatio ? failedConstLocations : failedEnumLocations; - if (failedLocations.length === 0 && !typeFiltered) { + if (constEnumLocations.length === 0 && failedTypeLocations.length === 0) { + return []; + } + + if (constEnumLocations.length > 0 && failedLocations.length === 0 && !typeFiltered) { return []; } + if (allowedTypes.has("number")) { + allowedTypes.delete("integer"); + } + + if (constEnumLocations.length === 0) { + if (allowedTypes.size === 0) { + return [{ + message: localization.getBooleanSchemaErrorMessage(), + instanceLocation: Instance.uri(instance), + schemaLocations: failedTypeLocations + }]; + } + + return [{ + message: localization.getTypeErrorMessage([...allowedTypes]), + instanceLocation: Instance.uri(instance), + schemaLocations: failedTypeLocations + }]; + } + if (allowedJson?.size === 0) { return [{ message: localization.getBooleanSchemaErrorMessage(), @@ -126,7 +118,7 @@ const typeConstEnumErrorHandler = async (normalizedErrors, instance, localizatio * @returns {Promise<{ allowedTypes: Set; failedTypeLocations: string[] }>} */ async function resolveTypes(normalizedErrors) { - let allowedTypes = ALL_TYPES; + let allowedTypes = new Set(ALL_TYPES); /** @type {string[]} */ const failedTypeLocations = []; @@ -148,22 +140,7 @@ async function resolveTypes(normalizedErrors) { } } - if (allowedTypes.has("number")) { - allowedTypes.delete("integer"); - } - return { allowedTypes, failedTypeLocations }; } -/** - * @param {Json} value - * @returns {string} - */ -function jsonSchemaType(value) { - if (value === null) return "null"; - if (Array.isArray(value)) return "array"; - if (typeof value === "number") return Number.isInteger(value) ? "integer" : "number"; - return typeof value; -} - export default typeConstEnumErrorHandler; From 12027c5c84851c90d8a8c667608009b126f0a77a Mon Sep 17 00:00:00 2001 From: Jason Desrosiers Date: Wed, 25 Feb 2026 13:25:41 -0800 Subject: [PATCH 6/6] Final cleanup --- src/error-handlers/typeConstEnum.js | 112 +++++++++++----------------- 1 file changed, 42 insertions(+), 70 deletions(-) diff --git a/src/error-handlers/typeConstEnum.js b/src/error-handlers/typeConstEnum.js index a27f17d..0a0441b 100644 --- a/src/error-handlers/typeConstEnum.js +++ b/src/error-handlers/typeConstEnum.js @@ -4,14 +4,36 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; import jsonStringify from "json-stringify-deterministic"; /** - * @import { ErrorHandler, InstanceOutput, Json } from "../index.d.ts" + * @import { ErrorHandler, Json } from "../index.d.ts" */ const ALL_TYPES = new Set(["null", "boolean", "number", "string", "array", "object", "integer"]); /** @type {ErrorHandler} */ const typeConstEnumErrorHandler = async (normalizedErrors, instance, localization) => { - const { allowedTypes, failedTypeLocations } = await resolveTypes(normalizedErrors); + let allowedTypes = new Set(ALL_TYPES); + /** @type {string[]} */ + const failedTypeLocations = []; + + for (const schemaLocation in normalizedErrors["https://json-schema.org/keyword/type"]) { + if (!normalizedErrors["https://json-schema.org/keyword/type"][schemaLocation]) { + failedTypeLocations.push(schemaLocation); + + const keyword = await getSchema(schemaLocation); + /** @type {string | string[]} */ + const value = Schema.value(keyword); + const types = Array.isArray(value) ? value : [value]; + /** @type {Set} */ + const keywordTypes = new Set(types); + if (keywordTypes.has("number")) { + keywordTypes.add("integer"); + } + allowedTypes = allowedTypes.intersection(keywordTypes); + } + } + if (allowedTypes.has("number")) { + allowedTypes.delete("integer"); + } /** @type {Set | undefined} */ let allowedJson; @@ -64,83 +86,33 @@ const typeConstEnumErrorHandler = async (normalizedErrors, instance, localizatio ? failedConstLocations : failedEnumLocations; - if (constEnumLocations.length === 0 && failedTypeLocations.length === 0) { + if (failedLocations.length === 0 && failedTypeLocations.length === 0) { return []; - } - - if (constEnumLocations.length > 0 && failedLocations.length === 0 && !typeFiltered) { - return []; - } - - if (allowedTypes.has("number")) { - allowedTypes.delete("integer"); - } - - if (constEnumLocations.length === 0) { - if (allowedTypes.size === 0) { - return [{ - message: localization.getBooleanSchemaErrorMessage(), - instanceLocation: Instance.uri(instance), - schemaLocations: failedTypeLocations - }]; - } - + } else if (allowedTypes.size === 0 || allowedJson?.size === 0) { return [{ - message: localization.getTypeErrorMessage([...allowedTypes]), + message: localization.getBooleanSchemaErrorMessage(), instanceLocation: Instance.uri(instance), - schemaLocations: failedTypeLocations + schemaLocations: [...failedTypeLocations, ...constEnumLocations] }]; - } + } else if (allowedJson?.size) { + /** @type Json[] */ + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + const allowedValues = [...allowedJson ?? []].map((json) => JSON.parse(json)); - if (allowedJson?.size === 0) { return [{ - message: localization.getBooleanSchemaErrorMessage(), + message: localization.getEnumErrorMessage(allowedValues), instanceLocation: Instance.uri(instance), - schemaLocations: [...failedTypeLocations, ...constEnumLocations] + schemaLocations: typeFiltered + ? [...failedTypeLocations, ...constEnumLocations] + : failedLocations + }]; + } else { + return [{ + message: localization.getTypeErrorMessage([...allowedTypes]), + instanceLocation: Instance.uri(instance), + schemaLocations: failedTypeLocations }]; } - - /** @type Json[] */ - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - const allowedValues = [...allowedJson ?? []].map((json) => JSON.parse(json)); - - return [{ - message: localization.getEnumErrorMessage(allowedValues), - instanceLocation: Instance.uri(instance), - schemaLocations: typeFiltered - ? [...failedTypeLocations, ...constEnumLocations] - : failedLocations - }]; }; -/** - * @param {InstanceOutput} normalizedErrors - * @returns {Promise<{ allowedTypes: Set; failedTypeLocations: string[] }>} - */ -async function resolveTypes(normalizedErrors) { - let allowedTypes = new Set(ALL_TYPES); - /** @type {string[]} */ - const failedTypeLocations = []; - - for (const schemaLocation in normalizedErrors["https://json-schema.org/keyword/type"]) { - const isValid = normalizedErrors["https://json-schema.org/keyword/type"][schemaLocation]; - if (!isValid) { - failedTypeLocations.push(schemaLocation); - - const keyword = await getSchema(schemaLocation); - /** @type {string | string[]} */ - const value = Schema.value(keyword); - const types = Array.isArray(value) ? value : [value]; - /** @type {Set} */ - const keywordTypes = new Set(types); - if (keywordTypes.has("number")) { - keywordTypes.add("integer"); - } - allowedTypes = allowedTypes.intersection(keywordTypes); - } - } - - return { allowedTypes, failedTypeLocations }; -} - export default typeConstEnumErrorHandler;