From 61afc118ae6daac6f88075fb513c1221c2a460b0 Mon Sep 17 00:00:00 2001 From: animesh sahoo Date: Thu, 12 Mar 2026 19:21:31 +0530 Subject: [PATCH 01/11] filter anyOf and oneOf alternative --- src/error-handlers/anyOf.js | 73 ++++++++++++++++++----- src/error-handlers/oneOf.js | 116 +++++++++++++++++++++++++++++------- 2 files changed, 154 insertions(+), 35 deletions(-) diff --git a/src/error-handlers/anyOf.js b/src/error-handlers/anyOf.js index d0aa801..622926b 100644 --- a/src/error-handlers/anyOf.js +++ b/src/error-handlers/anyOf.js @@ -2,45 +2,88 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; import { getErrors } from "../json-schema-errors.js"; /** - * @import { ErrorHandler, ErrorObject } from "../index.d.ts" + * @import { ErrorHandler, ErrorObject, NormalizedOutput } from "../index.d.ts" */ +/** @type (alternative: NormalizedOutput, propLocation: string) => boolean */ +const propertyPasses = (alternative, propLocation) => { + const propOutput = alternative[propLocation]; + if (!propOutput || Object.keys(propOutput).length === 0) return false; + return Object.values(propOutput).every((keywordResults) => + Object.values(keywordResults).every((v) => v === true) + ); +}; + /** @type ErrorHandler */ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => { /** @type ErrorObject[] */ const errors = []; - for (const schemaLocation in normalizedErrors["https://json-schema.org/keyword/anyOf"]) { - const anyOf = normalizedErrors["https://json-schema.org/keyword/anyOf"][schemaLocation]; + for (const schemaLocation in normalizedErrors[ + "https://json-schema.org/keyword/anyOf" + ]) { + const anyOf + = normalizedErrors["https://json-schema.org/keyword/anyOf"][schemaLocation]; if (typeof anyOf === "boolean") { continue; } - - const alternatives = []; const instanceLocation = Instance.uri(instance); + let filtered = anyOf; + + if (Instance.typeOf(instance) === "object") { + const instanceProps = new Set( + [...Instance.keys(instance)].map( + (keyNode) => /** @type {string} */ (Instance.value(keyNode)) + ) + ); + const prefix = `${instanceLocation}/`; + + filtered = filtered.filter((alternative) => { + const declaredProps = Object.keys(alternative) + .filter((loc) => loc.startsWith(prefix)) + .map((loc) => loc.slice(prefix.length)); + + if (declaredProps.length === 0) return true; + return declaredProps.some((prop) => instanceProps.has(prop)); + }); - for (const alternative of anyOf) { - const typeErrors = alternative[instanceLocation]?.["https://json-schema.org/keyword/type"]; - const match = !typeErrors || Object.values(typeErrors).every((isValid) => isValid); + filtered = filtered.filter((alternative) => + [...instanceProps].some((prop) => + propertyPasses(alternative, `${instanceLocation}/${prop}`) + ) + ); - if (match) { - alternatives.push(await getErrors(alternative, instance, localization)); + if (filtered.length === 0) { + filtered = anyOf; } - } + } else { + filtered = filtered.filter((alternative) => { + const typeResults + = alternative[instanceLocation]?.[ + "https://json-schema.org/keyword/type" + ]; + return ( + !typeResults || Object.values(typeResults).every((isValid) => isValid) + ); + }); - if (alternatives.length === 0) { - for (const alternative of anyOf) { - alternatives.push(await getErrors(alternative, instance, localization)); + if (filtered.length === 0) { + filtered = anyOf; } } + const alternatives = []; + for (const alternative of filtered) { + alternatives.push(await getErrors(alternative, instance, localization)); + } + if (alternatives.length === 1) { errors.push(...alternatives[0]); } else { errors.push({ message: localization.getAnyOfErrorMessage(), alternatives: alternatives, - instanceLocation: Instance.uri(instance), + instanceLocation, schemaLocations: [schemaLocation] }); } diff --git a/src/error-handlers/oneOf.js b/src/error-handlers/oneOf.js index d0cd83b..ea57b70 100644 --- a/src/error-handlers/oneOf.js +++ b/src/error-handlers/oneOf.js @@ -2,52 +2,128 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; import { getErrors } from "../json-schema-errors.js"; /** - * @import { ErrorHandler, ErrorObject } from "../index.d.ts" + * @import { ErrorHandler, ErrorObject, NormalizedOutput } from "../index.d.ts" */ +/** @type (alternative: NormalizedOutput, propLocation: string) => boolean */ +const propertyPasses = (alternative, propLocation) => { + const propOutput = alternative[propLocation]; + if (!propOutput || Object.keys(propOutput).length === 0) return false; + return Object.values(propOutput).every((keywordResults) => + Object.values(keywordResults).every((v) => v === true) + ); +}; + /** @type ErrorHandler */ const oneOfErrorHandler = async (normalizedErrors, instance, localization) => { /** @type ErrorObject[] */ const errors = []; - for (const schemaLocation in normalizedErrors["https://json-schema.org/keyword/oneOf"]) { - const oneOf = normalizedErrors["https://json-schema.org/keyword/oneOf"][schemaLocation]; + for (const schemaLocation in normalizedErrors[ + "https://json-schema.org/keyword/oneOf" + ]) { + const oneOf + = normalizedErrors["https://json-schema.org/keyword/oneOf"][schemaLocation]; if (typeof oneOf === "boolean") { continue; } - const alternatives = []; const instanceLocation = Instance.uri(instance); + let matchCount = 0; + /** @type ErrorObject[][] */ + const failingAlternatives = []; for (const alternative of oneOf) { - const typeErrors = alternative[instanceLocation]?.["https://json-schema.org/keyword/type"]; - const match = !typeErrors || Object.values(typeErrors).every((isValid) => isValid); - - if (match) { - const alternativeErrors = await getErrors(alternative, instance, localization); - if (alternativeErrors.length) { - alternatives.push(alternativeErrors); - } else { - matchCount++; - } + const alternativeErrors = await getErrors( + alternative, + instance, + localization + ); + if (alternativeErrors.length) { + failingAlternatives.push(alternativeErrors); + } else { + matchCount++; } } - if (matchCount === 0 && alternatives.length === 0) { - for (const alternative of oneOf) { - const alternativeErrors = await getErrors(alternative, instance, localization); + if (matchCount > 1) { + /** @type ErrorObject */ + const error = { + message: localization.getOneOfErrorMessage(matchCount), + instanceLocation, + schemaLocations: [schemaLocation] + }; + if (failingAlternatives.length) { + error.alternatives = failingAlternatives; + } + errors.push(error); + continue; + } + + let filtered = oneOf; + + if (Instance.typeOf(instance) === "object") { + const instanceProps = new Set( + [...Instance.keys(instance)].map( + (keyNode) => /** @type {string} */ (Instance.value(keyNode)) + ) + ); + const prefix = `${instanceLocation}/`; + + filtered = filtered.filter((alternative) => { + const declaredProps = Object.keys(alternative) + .filter((loc) => loc.startsWith(prefix)) + .map((loc) => loc.slice(prefix.length)); + + if (declaredProps.length === 0) return true; + return declaredProps.some((prop) => instanceProps.has(prop)); + }); + + filtered = filtered.filter((alternative) => + [...instanceProps].some((prop) => + propertyPasses(alternative, `${instanceLocation}/${prop}`) + ) + ); + + if (filtered.length === 0) { + filtered = oneOf; + } + } else { + filtered = filtered.filter((alternative) => { + const typeResults + = alternative[instanceLocation]?.[ + "https://json-schema.org/keyword/type" + ]; + return ( + !typeResults || Object.values(typeResults).every((isValid) => isValid) + ); + }); + + if (filtered.length === 0) { + filtered = oneOf; + } + } + + const alternatives = []; + for (const alternative of filtered) { + const alternativeErrors = await getErrors( + alternative, + instance, + localization + ); + if (alternativeErrors.length) { alternatives.push(alternativeErrors); } } - if (alternatives.length === 1 && matchCount === 0) { + if (alternatives.length === 1) { errors.push(...alternatives[0]); } else { /** @type ErrorObject */ const alternativeErrors = { - message: localization.getOneOfErrorMessage(matchCount), - instanceLocation: Instance.uri(instance), + message: localization.getOneOfErrorMessage(0), + instanceLocation, schemaLocations: [schemaLocation] }; if (alternatives.length) { From a37a7b5e1593426b76538a12ead1e9e0e413b7a5 Mon Sep 17 00:00:00 2001 From: animesh sahoo Date: Sat, 14 Mar 2026 21:55:49 +0530 Subject: [PATCH 02/11] added tests for new behavior --- src/test-suite/tests/anyOf.json | 146 ++++++++++++++++++++++++++++++ src/test-suite/tests/oneOf.json | 152 ++++++++++++++++++++++++++++++++ 2 files changed, 298 insertions(+) diff --git a/src/test-suite/tests/anyOf.json b/src/test-suite/tests/anyOf.json index 8998ad9..925e6c1 100644 --- a/src/test-suite/tests/anyOf.json +++ b/src/test-suite/tests/anyOf.json @@ -253,6 +253,152 @@ "schemaLocations": ["https://example.com/main#/anyOf"] } ] + }, + { + "description": "anyOf object alternatives keep only one branch when only one branch has a passing instance property", + "compatibility": "6", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { "const": "a" }, + "a": { "type": "string" } + }, + "required": ["type", "a"] + }, + { + "type": "object", + "properties": { + "type": { "const": "b" }, + "b": { "type": "number" } + }, + "required": ["type", "b"] + } + ] + }, + "instance": { "type": "b", "b": "oops" }, + "errors": [ + { + "messageId": "type-message", + "messageParams": { + "expectedTypes": "number" + }, + "instanceLocation": "#/b", + "schemaLocations": ["#/anyOf/1/properties/b/type"] + } + ] + }, + { + "description": "anyOf object alternatives keep both branches when each branch has at least one passing instance property", + "compatibility": "6", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { "const": "a" }, + "x": { "type": "string" } + }, + "required": ["type", "x"] + }, + { + "type": "object", + "properties": { + "type": { + "allOf": [ + { "type": "string" }, + { "minLength": 2 } + ] + }, + "x": { "type": "number" } + }, + "required": ["type", "x"] + } + ] + }, + "instance": { "type": "a", "x": 42 }, + "errors": [ + { + "messageId": "anyOf-message", + "alternatives": [ + [ + { + "messageId": "type-message", + "messageParams": { + "expectedTypes": "string" + }, + "instanceLocation": "#/x", + "schemaLocations": ["#/anyOf/0/properties/x/type"] + } + ], + [ + { + "messageId": "minLength-message", + "messageParams": { + "minLength": "2" + }, + "instanceLocation": "#/type", + "schemaLocations": ["#/anyOf/1/properties/type/allOf/1/minLength"] + } + ] + ], + "instanceLocation": "#", + "schemaLocations": ["#/anyOf"] + } + ] + }, + { + "description": "anyOf object alternatives fallback to all when no instance property passes in any branch", + "compatibility": "6", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "kind": { "const": "a" } + }, + "required": ["kind"] + }, + { + "type": "object", + "properties": { + "kind": { "const": "b" } + }, + "required": ["kind"] + } + ] + }, + "instance": { "kind": "c" }, + "errors": [ + { + "messageId": "anyOf-message", + "alternatives": [ + [ + { + "messageId": "const-message", + "messageParams": { + "expected": "\"a\"" + }, + "instanceLocation": "#/kind", + "schemaLocations": ["#/anyOf/0/properties/kind/const"] + } + ], + [ + { + "messageId": "const-message", + "messageParams": { + "expected": "\"b\"" + }, + "instanceLocation": "#/kind", + "schemaLocations": ["#/anyOf/1/properties/kind/const"] + } + ] + ], + "instanceLocation": "#", + "schemaLocations": ["#/anyOf"] + } + ] } ] } diff --git a/src/test-suite/tests/oneOf.json b/src/test-suite/tests/oneOf.json index 05c1f4b..096353d 100644 --- a/src/test-suite/tests/oneOf.json +++ b/src/test-suite/tests/oneOf.json @@ -321,6 +321,158 @@ "schemaLocations": ["https://example.com/main#/oneOf"] } ] + }, + { + "description": "oneOf object alternatives keep only one branch when only one branch has a passing instance property", + "compatibility": "6", + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { "const": "a" }, + "a": { "type": "string" } + }, + "required": ["type", "a"] + }, + { + "type": "object", + "properties": { + "type": { "const": "b" }, + "b": { "type": "number" } + }, + "required": ["type", "b"] + } + ] + }, + "instance": { "type": "b", "b": "oops" }, + "errors": [ + { + "messageId": "type-message", + "messageParams": { + "expectedTypes": "number" + }, + "instanceLocation": "#/b", + "schemaLocations": ["#/oneOf/1/properties/b/type"] + } + ] + }, + { + "description": "oneOf object alternatives fallback to all when no instance property passes in any branch", + "compatibility": "6", + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "kind": { "const": "a" } + }, + "required": ["kind"] + }, + { + "type": "object", + "properties": { + "kind": { "const": "b" } + }, + "required": ["kind"] + } + ] + }, + "instance": { "kind": "c" }, + "errors": [ + { + "messageId": "oneOf-message", + "messageParams": { + "matchCount": 0 + }, + "alternatives": [ + [ + { + "messageId": "const-message", + "messageParams": { + "expected": "\"a\"" + }, + "instanceLocation": "#/kind", + "schemaLocations": ["#/oneOf/0/properties/kind/const"] + } + ], + [ + { + "messageId": "const-message", + "messageParams": { + "expected": "\"b\"" + }, + "instanceLocation": "#/kind", + "schemaLocations": ["#/oneOf/1/properties/kind/const"] + } + ] + ], + "instanceLocation": "#", + "schemaLocations": ["#/oneOf"] + } + ] + }, + { + "description": "oneOf object alternatives keep both branches when each branch has at least one passing instance property", + "compatibility": "6", + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { "const": "a" }, + "x": { "type": "string" } + }, + "required": ["type", "x"] + }, + { + "type": "object", + "properties": { + "type": { + "allOf": [ + { "type": "string" }, + { "minLength": 2 } + ] + }, + "x": { "type": "number" } + }, + "required": ["type", "x"] + } + ] + }, + "instance": { "type": "a", "x": 42 }, + "errors": [ + { + "messageId": "oneOf-message", + "messageParams": { + "matchCount": 0 + }, + "alternatives": [ + [ + { + "messageId": "type-message", + "messageParams": { + "expectedTypes": "string" + }, + "instanceLocation": "#/x", + "schemaLocations": ["#/oneOf/0/properties/x/type"] + } + ], + [ + { + "messageId": "minLength-message", + "messageParams": { + "minLength": "2" + }, + "instanceLocation": "#/type", + "schemaLocations": ["#/oneOf/1/properties/type/allOf/1/minLength"] + } + ] + ], + "instanceLocation": "#", + "schemaLocations": ["#/oneOf"] + } + ] } ] } From fd298bcb07df15fd58feffc79d1d7324f95c6395 Mon Sep 17 00:00:00 2001 From: animesh sahoo Date: Sat, 21 Mar 2026 04:21:50 +0530 Subject: [PATCH 03/11] used hyperjump pact for better performance --- src/error-handlers/anyOf.js | 82 ++++++++++++++++---------------- src/error-handlers/oneOf.js | 94 +++++++++++++++++-------------------- 2 files changed, 84 insertions(+), 92 deletions(-) diff --git a/src/error-handlers/anyOf.js b/src/error-handlers/anyOf.js index 622926b..d0babae 100644 --- a/src/error-handlers/anyOf.js +++ b/src/error-handlers/anyOf.js @@ -1,29 +1,19 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; +import * as JsonPointer from "@hyperjump/json-pointer"; +import * as Pact from "@hyperjump/pact"; import { getErrors } from "../json-schema-errors.js"; /** * @import { ErrorHandler, ErrorObject, NormalizedOutput } from "../index.d.ts" */ -/** @type (alternative: NormalizedOutput, propLocation: string) => boolean */ -const propertyPasses = (alternative, propLocation) => { - const propOutput = alternative[propLocation]; - if (!propOutput || Object.keys(propOutput).length === 0) return false; - return Object.values(propOutput).every((keywordResults) => - Object.values(keywordResults).every((v) => v === true) - ); -}; - /** @type ErrorHandler */ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => { /** @type ErrorObject[] */ const errors = []; - for (const schemaLocation in normalizedErrors[ - "https://json-schema.org/keyword/anyOf" - ]) { - const anyOf - = normalizedErrors["https://json-schema.org/keyword/anyOf"][schemaLocation]; + for (const schemaLocation in normalizedErrors["https://json-schema.org/keyword/anyOf"]) { + const anyOf = normalizedErrors["https://json-schema.org/keyword/anyOf"][schemaLocation]; if (typeof anyOf === "boolean") { continue; } @@ -31,47 +21,49 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => { let filtered = anyOf; if (Instance.typeOf(instance) === "object") { - const instanceProps = new Set( - [...Instance.keys(instance)].map( - (keyNode) => /** @type {string} */ (Instance.value(keyNode)) + const instanceProps = Pact.collectSet( + Pact.map( + (keyNode) => /** @type {string} */ (Instance.value(keyNode)), + Instance.keys(instance) ) ); const prefix = `${instanceLocation}/`; - filtered = filtered.filter((alternative) => { + filtered = []; + for (const alternative of anyOf) { + const typeResults = alternative[instanceLocation]?.["https://json-schema.org/keyword/type"]; + if (typeResults && !Object.values(typeResults).every((isValid) => isValid)) { + continue; + } + const declaredProps = Object.keys(alternative) .filter((loc) => loc.startsWith(prefix)) - .map((loc) => loc.slice(prefix.length)); + .map((loc) => /** @type {string} */ (Pact.head(JsonPointer.pointerSegments(loc.slice(prefix.length - 1))))); - if (declaredProps.length === 0) return true; - return declaredProps.some((prop) => instanceProps.has(prop)); - }); + if (declaredProps.length > 0 && !declaredProps.some((prop) => instanceProps.has(prop))) { + continue; + } - filtered = filtered.filter((alternative) => - [...instanceProps].some((prop) => - propertyPasses(alternative, `${instanceLocation}/${prop}`) - ) - ); + if (!Pact.some((prop) => propertyPasses(alternative[JsonPointer.append(prop, instanceLocation)]), instanceProps)) { + continue; + } - if (filtered.length === 0) { - filtered = anyOf; + filtered.push(alternative); } } else { - filtered = filtered.filter((alternative) => { - const typeResults - = alternative[instanceLocation]?.[ - "https://json-schema.org/keyword/type" - ]; - return ( - !typeResults || Object.values(typeResults).every((isValid) => isValid) - ); - }); - - if (filtered.length === 0) { - filtered = anyOf; + filtered = []; + for (const alternative of anyOf) { + const typeResults = alternative[instanceLocation]["https://json-schema.org/keyword/type"]; + if (!typeResults || Object.values(typeResults).every((isValid) => isValid)) { + filtered.push(alternative); + } } } + if (filtered.length === 0) { + filtered = anyOf; + } + const alternatives = []; for (const alternative of filtered) { alternatives.push(await getErrors(alternative, instance, localization)); @@ -92,4 +84,12 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => { return errors; }; +/** @type (propOutput: NormalizedOutput[string] | undefined) => boolean */ +const propertyPasses = (propOutput) => { + if (!propOutput || Object.keys(propOutput).length === 0) return false; + return Object.values(propOutput).every((keywordResults) => + Object.values(keywordResults).every((v) => v === true) + ); +}; + export default anyOfErrorHandler; diff --git a/src/error-handlers/oneOf.js b/src/error-handlers/oneOf.js index ea57b70..1447589 100644 --- a/src/error-handlers/oneOf.js +++ b/src/error-handlers/oneOf.js @@ -1,29 +1,19 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; +import * as JsonPointer from "@hyperjump/json-pointer"; +import * as Pact from "@hyperjump/pact"; import { getErrors } from "../json-schema-errors.js"; /** * @import { ErrorHandler, ErrorObject, NormalizedOutput } from "../index.d.ts" */ -/** @type (alternative: NormalizedOutput, propLocation: string) => boolean */ -const propertyPasses = (alternative, propLocation) => { - const propOutput = alternative[propLocation]; - if (!propOutput || Object.keys(propOutput).length === 0) return false; - return Object.values(propOutput).every((keywordResults) => - Object.values(keywordResults).every((v) => v === true) - ); -}; - /** @type ErrorHandler */ const oneOfErrorHandler = async (normalizedErrors, instance, localization) => { /** @type ErrorObject[] */ const errors = []; - for (const schemaLocation in normalizedErrors[ - "https://json-schema.org/keyword/oneOf" - ]) { - const oneOf - = normalizedErrors["https://json-schema.org/keyword/oneOf"][schemaLocation]; + for (const schemaLocation in normalizedErrors["https://json-schema.org/keyword/oneOf"]) { + const oneOf = normalizedErrors["https://json-schema.org/keyword/oneOf"][schemaLocation]; if (typeof oneOf === "boolean") { continue; } @@ -35,11 +25,7 @@ const oneOfErrorHandler = async (normalizedErrors, instance, localization) => { const failingAlternatives = []; for (const alternative of oneOf) { - const alternativeErrors = await getErrors( - alternative, - instance, - localization - ); + const alternativeErrors = await getErrors(alternative, instance, localization); if (alternativeErrors.length) { failingAlternatives.push(alternativeErrors); } else { @@ -64,54 +50,52 @@ const oneOfErrorHandler = async (normalizedErrors, instance, localization) => { let filtered = oneOf; if (Instance.typeOf(instance) === "object") { - const instanceProps = new Set( - [...Instance.keys(instance)].map( - (keyNode) => /** @type {string} */ (Instance.value(keyNode)) + const instanceProps = Pact.collectSet( + Pact.map( + (keyNode) => /** @type {string} */ (Instance.value(keyNode)), + Instance.keys(instance) ) ); const prefix = `${instanceLocation}/`; - filtered = filtered.filter((alternative) => { + filtered = []; + for (const alternative of oneOf) { + const typeResults = alternative[instanceLocation]?.["https://json-schema.org/keyword/type"]; + if (typeResults && !Object.values(typeResults).every((isValid) => isValid)) { + continue; + } + const declaredProps = Object.keys(alternative) .filter((loc) => loc.startsWith(prefix)) - .map((loc) => loc.slice(prefix.length)); + .map((loc) => /** @type {string} */ (Pact.head(JsonPointer.pointerSegments(loc.slice(prefix.length - 1))))); - if (declaredProps.length === 0) return true; - return declaredProps.some((prop) => instanceProps.has(prop)); - }); + if (declaredProps.length > 0 && !declaredProps.some((prop) => instanceProps.has(prop))) { + continue; + } - filtered = filtered.filter((alternative) => - [...instanceProps].some((prop) => - propertyPasses(alternative, `${instanceLocation}/${prop}`) - ) - ); + if (!Pact.some((prop) => propertyPasses(alternative[JsonPointer.append(prop, instanceLocation)]), instanceProps)) { + continue; + } - if (filtered.length === 0) { - filtered = oneOf; + filtered.push(alternative); } } else { - filtered = filtered.filter((alternative) => { - const typeResults - = alternative[instanceLocation]?.[ - "https://json-schema.org/keyword/type" - ]; - return ( - !typeResults || Object.values(typeResults).every((isValid) => isValid) - ); - }); - - if (filtered.length === 0) { - filtered = oneOf; + filtered = []; + for (const alternative of oneOf) { + const typeResults = alternative[instanceLocation]["https://json-schema.org/keyword/type"]; + if (!typeResults || Object.values(typeResults).every((isValid) => isValid)) { + filtered.push(alternative); + } } } + if (filtered.length === 0) { + filtered = oneOf; + } + const alternatives = []; for (const alternative of filtered) { - const alternativeErrors = await getErrors( - alternative, - instance, - localization - ); + const alternativeErrors = await getErrors(alternative, instance, localization); if (alternativeErrors.length) { alternatives.push(alternativeErrors); } @@ -136,4 +120,12 @@ const oneOfErrorHandler = async (normalizedErrors, instance, localization) => { return errors; }; +/** @type (propOutput: NormalizedOutput[string] | undefined) => boolean */ +const propertyPasses = (propOutput) => { + if (!propOutput || Object.keys(propOutput).length === 0) return false; + return Object.values(propOutput).every((keywordResults) => + Object.values(keywordResults).every((v) => v === true) + ); +}; + export default oneOfErrorHandler; From a7cb928205a8cdd1f49950e7fd52794881a030e9 Mon Sep 17 00:00:00 2001 From: animesh sahoo Date: Mon, 23 Mar 2026 04:15:29 +0530 Subject: [PATCH 04/11] fixed linting issues with oxlint --- src/error-handlers/anyOf.js | 8 ++++---- src/error-handlers/oneOf.js | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/error-handlers/anyOf.js b/src/error-handlers/anyOf.js index d0babae..d4f6836 100644 --- a/src/error-handlers/anyOf.js +++ b/src/error-handlers/anyOf.js @@ -86,10 +86,10 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => { /** @type (propOutput: NormalizedOutput[string] | undefined) => boolean */ const propertyPasses = (propOutput) => { - if (!propOutput || Object.keys(propOutput).length === 0) return false; - return Object.values(propOutput).every((keywordResults) => - Object.values(keywordResults).every((v) => v === true) - ); + if (!propOutput || Object.keys(propOutput).length === 0) { + return false; + } + return Object.values(propOutput).every((keywordResults) => Object.values(keywordResults).every((v) => v === true)); }; export default anyOfErrorHandler; diff --git a/src/error-handlers/oneOf.js b/src/error-handlers/oneOf.js index 1447589..ddf63c0 100644 --- a/src/error-handlers/oneOf.js +++ b/src/error-handlers/oneOf.js @@ -122,10 +122,10 @@ const oneOfErrorHandler = async (normalizedErrors, instance, localization) => { /** @type (propOutput: NormalizedOutput[string] | undefined) => boolean */ const propertyPasses = (propOutput) => { - if (!propOutput || Object.keys(propOutput).length === 0) return false; - return Object.values(propOutput).every((keywordResults) => - Object.values(keywordResults).every((v) => v === true) - ); + if (!propOutput || Object.keys(propOutput).length === 0) { + return false; + } + return Object.values(propOutput).every((keywordResults) => Object.values(keywordResults).every((v) => v === true)); }; export default oneOfErrorHandler; From 6ade7066853c5b030f058cf6cff725d9f65d2ae8 Mon Sep 17 00:00:00 2001 From: animesh sahoo Date: Sun, 29 Mar 2026 23:19:54 +0530 Subject: [PATCH 05/11] changed the logic to match what is discussed --- src/error-handlers/anyOf.js | 73 +++++++++++++++++--------------- src/error-handlers/oneOf.js | 75 ++++++++++++++++++--------------- src/test-suite/tests/anyOf.json | 33 +++++++++++++++ src/test-suite/tests/oneOf.json | 33 +++++++++++++++ 4 files changed, 147 insertions(+), 67 deletions(-) diff --git a/src/error-handlers/anyOf.js b/src/error-handlers/anyOf.js index d4f6836..d23efa5 100644 --- a/src/error-handlers/anyOf.js +++ b/src/error-handlers/anyOf.js @@ -20,50 +20,57 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => { const instanceLocation = Instance.uri(instance); let filtered = anyOf; - if (Instance.typeOf(instance) === "object") { - const instanceProps = Pact.collectSet( - Pact.map( - (keyNode) => /** @type {string} */ (Instance.value(keyNode)), - Instance.keys(instance) - ) - ); - const prefix = `${instanceLocation}/`; - - filtered = []; - for (const alternative of anyOf) { - const typeResults = alternative[instanceLocation]?.["https://json-schema.org/keyword/type"]; - if (typeResults && !Object.values(typeResults).every((isValid) => isValid)) { - continue; - } + const isObject = Instance.typeOf(instance) === "object"; + const instanceProps = isObject + ? Pact.collectSet(Pact.map((keyNode) => /** @type {string} */ (Instance.value(keyNode)), Instance.keys(instance))) + : undefined; + const prefix = `${instanceLocation}/`; - const declaredProps = Object.keys(alternative) - .filter((loc) => loc.startsWith(prefix)) - .map((loc) => /** @type {string} */ (Pact.head(JsonPointer.pointerSegments(loc.slice(prefix.length - 1))))); + filtered = []; + for (const alternative of anyOf) { + const typeResults = alternative[instanceLocation]?.["https://json-schema.org/keyword/type"]; + if (typeResults && !Object.values(typeResults).every((isValid) => isValid)) { + continue; + } - if (declaredProps.length > 0 && !declaredProps.some((prop) => instanceProps.has(prop))) { - continue; - } + if (isObject) { + const declaredProps = Pact.map( + (loc) => /** @type {string} */ (Pact.head(JsonPointer.pointerSegments(loc.slice(prefix.length - 1)))), + Pact.filter((loc) => loc.startsWith(prefix), Object.keys(alternative)) + ); - if (!Pact.some((prop) => propertyPasses(alternative[JsonPointer.append(prop, instanceLocation)]), instanceProps)) { + let hasDeclaredProps = false; + const hasMatchingProp = Pact.some((prop) => { + hasDeclaredProps = true; + return /** @type {Set} */ (instanceProps).has(prop); + }, declaredProps); + if (hasDeclaredProps && !hasMatchingProp) { continue; } - - filtered.push(alternative); - } - } else { - filtered = []; - for (const alternative of anyOf) { - const typeResults = alternative[instanceLocation]["https://json-schema.org/keyword/type"]; - if (!typeResults || Object.values(typeResults).every((isValid) => isValid)) { - filtered.push(alternative); - } } + + filtered.push(alternative); } if (filtered.length === 0) { filtered = anyOf; } + if (isObject) { + const discriminators = Pact.collectSet( + Pact.filter( + (prop) => Pact.some((alternative) => propertyPasses(alternative[JsonPointer.append(prop, instanceLocation)]), filtered), + /** @type {Set} */ (instanceProps) + ) + ); + + const afterRule2 = Pact.collectArray(Pact.filter((alternative) => !Pact.some((prop) => !propertyPasses(alternative[JsonPointer.append(prop, instanceLocation)]), discriminators), filtered)); + + if (afterRule2.length > 0) { + filtered = afterRule2; + } + } + const alternatives = []; for (const alternative of filtered) { alternatives.push(await getErrors(alternative, instance, localization)); @@ -86,7 +93,7 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => { /** @type (propOutput: NormalizedOutput[string] | undefined) => boolean */ const propertyPasses = (propOutput) => { - if (!propOutput || Object.keys(propOutput).length === 0) { + if (!propOutput) { return false; } return Object.values(propOutput).every((keywordResults) => Object.values(keywordResults).every((v) => v === true)); diff --git a/src/error-handlers/oneOf.js b/src/error-handlers/oneOf.js index ddf63c0..09ed526 100644 --- a/src/error-handlers/oneOf.js +++ b/src/error-handlers/oneOf.js @@ -49,50 +49,57 @@ const oneOfErrorHandler = async (normalizedErrors, instance, localization) => { let filtered = oneOf; - if (Instance.typeOf(instance) === "object") { - const instanceProps = Pact.collectSet( - Pact.map( - (keyNode) => /** @type {string} */ (Instance.value(keyNode)), - Instance.keys(instance) - ) - ); - const prefix = `${instanceLocation}/`; - - filtered = []; - for (const alternative of oneOf) { - const typeResults = alternative[instanceLocation]?.["https://json-schema.org/keyword/type"]; - if (typeResults && !Object.values(typeResults).every((isValid) => isValid)) { - continue; - } - - const declaredProps = Object.keys(alternative) - .filter((loc) => loc.startsWith(prefix)) - .map((loc) => /** @type {string} */ (Pact.head(JsonPointer.pointerSegments(loc.slice(prefix.length - 1))))); + const isObject = Instance.typeOf(instance) === "object"; + const instanceProps = isObject + ? Pact.collectSet(Pact.map((keyNode) => /** @type {string} */ (Instance.value(keyNode)), Instance.keys(instance))) + : undefined; + const prefix = `${instanceLocation}/`; - if (declaredProps.length > 0 && !declaredProps.some((prop) => instanceProps.has(prop))) { - continue; - } + filtered = []; + for (const alternative of oneOf) { + const typeResults = alternative[instanceLocation]?.["https://json-schema.org/keyword/type"]; + if (typeResults && !Object.values(typeResults).every((isValid) => isValid)) { + continue; + } - if (!Pact.some((prop) => propertyPasses(alternative[JsonPointer.append(prop, instanceLocation)]), instanceProps)) { + if (isObject) { + const declaredProps = Pact.map( + (loc) => /** @type {string} */ (Pact.head(JsonPointer.pointerSegments(loc.slice(prefix.length - 1)))), + Pact.filter((loc) => loc.startsWith(prefix), Object.keys(alternative)) + ); + + let hasDeclaredProps = false; + const hasMatchingProp = Pact.some((prop) => { + hasDeclaredProps = true; + return /** @type {Set} */ (instanceProps).has(prop); + }, declaredProps); + if (hasDeclaredProps && !hasMatchingProp) { continue; } - - filtered.push(alternative); - } - } else { - filtered = []; - for (const alternative of oneOf) { - const typeResults = alternative[instanceLocation]["https://json-schema.org/keyword/type"]; - if (!typeResults || Object.values(typeResults).every((isValid) => isValid)) { - filtered.push(alternative); - } } + + filtered.push(alternative); } if (filtered.length === 0) { filtered = oneOf; } + if (isObject) { + const discriminators = Pact.collectSet( + Pact.filter( + (prop) => Pact.some((alternative) => propertyPasses(alternative[JsonPointer.append(prop, instanceLocation)]), filtered), + /** @type {Set} */ (instanceProps) + ) + ); + + const afterRule2 = Pact.collectArray(Pact.filter((alternative) => !Pact.some((prop) => !propertyPasses(alternative[JsonPointer.append(prop, instanceLocation)]), discriminators), filtered)); + + if (afterRule2.length > 0) { + filtered = afterRule2; + } + } + const alternatives = []; for (const alternative of filtered) { const alternativeErrors = await getErrors(alternative, instance, localization); @@ -122,7 +129,7 @@ const oneOfErrorHandler = async (normalizedErrors, instance, localization) => { /** @type (propOutput: NormalizedOutput[string] | undefined) => boolean */ const propertyPasses = (propOutput) => { - if (!propOutput || Object.keys(propOutput).length === 0) { + if (!propOutput) { return false; } return Object.values(propOutput).every((keywordResults) => Object.values(keywordResults).every((v) => v === true)); diff --git a/src/test-suite/tests/anyOf.json b/src/test-suite/tests/anyOf.json index 925e6c1..b377cf9 100644 --- a/src/test-suite/tests/anyOf.json +++ b/src/test-suite/tests/anyOf.json @@ -348,6 +348,39 @@ } ] }, + { + "description": "anyOf object alternatives filters to matching branch by declared property intersection", + "compatibility": "6", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "a": { "type": "string" } + }, + "required": ["a"] + }, + { + "type": "object", + "properties": { + "b": { "type": "string" } + }, + "required": ["b"] + } + ] + }, + "instance": { "a": 42 }, + "errors": [ + { + "messageId": "type-message", + "messageParams": { + "expectedTypes": "string" + }, + "instanceLocation": "#/a", + "schemaLocations": ["#/anyOf/0/properties/a/type"] + } + ] + }, { "description": "anyOf object alternatives fallback to all when no instance property passes in any branch", "compatibility": "6", diff --git a/src/test-suite/tests/oneOf.json b/src/test-suite/tests/oneOf.json index 096353d..f2038ac 100644 --- a/src/test-suite/tests/oneOf.json +++ b/src/test-suite/tests/oneOf.json @@ -357,6 +357,39 @@ } ] }, + { + "description": "oneOf object alternatives filters to matching branch by declared property intersection", + "compatibility": "6", + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "a": { "type": "string" } + }, + "required": ["a"] + }, + { + "type": "object", + "properties": { + "b": { "type": "string" } + }, + "required": ["b"] + } + ] + }, + "instance": { "a": 42 }, + "errors": [ + { + "messageId": "type-message", + "messageParams": { + "expectedTypes": "string" + }, + "instanceLocation": "#/a", + "schemaLocations": ["#/oneOf/0/properties/a/type"] + } + ] + }, { "description": "oneOf object alternatives fallback to all when no instance property passes in any branch", "compatibility": "6", From bf9fdc3d363161cc3cadcfafb1d755bd113ee1e4 Mon Sep 17 00:00:00 2001 From: animesh sahoo Date: Wed, 1 Apr 2026 16:09:35 +0530 Subject: [PATCH 06/11] simplified code --- src/error-handlers/anyOf.js | 52 ++++++++++++++++-------------- src/error-handlers/oneOf.js | 63 ++++++++++++++++++++----------------- 2 files changed, 63 insertions(+), 52 deletions(-) diff --git a/src/error-handlers/anyOf.js b/src/error-handlers/anyOf.js index d23efa5..92132bd 100644 --- a/src/error-handlers/anyOf.js +++ b/src/error-handlers/anyOf.js @@ -22,7 +22,7 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => { const isObject = Instance.typeOf(instance) === "object"; const instanceProps = isObject - ? Pact.collectSet(Pact.map((keyNode) => /** @type {string} */ (Instance.value(keyNode)), Instance.keys(instance))) + ? Pact.collectSet(Pact.pipe(Instance.keys(instance), Pact.map((keyNode) => /** @type {string} */ (Instance.value(keyNode))))) : undefined; const prefix = `${instanceLocation}/`; @@ -34,17 +34,13 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => { } if (isObject) { - const declaredProps = Pact.map( - (loc) => /** @type {string} */ (Pact.head(JsonPointer.pointerSegments(loc.slice(prefix.length - 1)))), - Pact.filter((loc) => loc.startsWith(prefix), Object.keys(alternative)) - ); - - let hasDeclaredProps = false; - const hasMatchingProp = Pact.some((prop) => { - hasDeclaredProps = true; - return /** @type {Set} */ (instanceProps).has(prop); - }, declaredProps); - if (hasDeclaredProps && !hasMatchingProp) { + const declaredProps = Pact.collectSet(Pact.pipe( + Object.keys(alternative), + Pact.filter((loc) => loc.startsWith(prefix)), + Pact.map((loc) => /** @type {string} */ (Pact.head(JsonPointer.pointerSegments(loc.slice(prefix.length - 1))))) + )); + + if (declaredProps.size > 0 && !Pact.some((prop) => declaredProps.has(prop), /** @type {Set} */ (instanceProps))) { continue; } } @@ -56,24 +52,32 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => { filtered = anyOf; } + /** @type ErrorObject[][] */ + const alternatives = []; + if (isObject) { const discriminators = Pact.collectSet( - Pact.filter( - (prop) => Pact.some((alternative) => propertyPasses(alternative[JsonPointer.append(prop, instanceLocation)]), filtered), - /** @type {Set} */ (instanceProps) - ) + /** @type {Iterable} */ (Pact.pipe( + filtered, + Pact.map((alternative) => Pact.pipe( + /** @type {Set} */ (instanceProps), + Pact.filter((prop) => propertyPasses(alternative[JsonPointer.append(prop, instanceLocation)])) + )), + Pact.flatten + )) ); - const afterRule2 = Pact.collectArray(Pact.filter((alternative) => !Pact.some((prop) => !propertyPasses(alternative[JsonPointer.append(prop, instanceLocation)]), discriminators), filtered)); - - if (afterRule2.length > 0) { - filtered = afterRule2; + for (const alternative of filtered) { + if (!Pact.some((prop) => !propertyPasses(alternative[JsonPointer.append(prop, instanceLocation)]), discriminators)) { + alternatives.push(await getErrors(alternative, instance, localization)); + } } } - const alternatives = []; - for (const alternative of filtered) { - alternatives.push(await getErrors(alternative, instance, localization)); + if (alternatives.length === 0) { + for (const alternative of filtered) { + alternatives.push(await getErrors(alternative, instance, localization)); + } } if (alternatives.length === 1) { @@ -81,7 +85,7 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => { } else { errors.push({ message: localization.getAnyOfErrorMessage(), - alternatives: alternatives, + alternatives, instanceLocation, schemaLocations: [schemaLocation] }); diff --git a/src/error-handlers/oneOf.js b/src/error-handlers/oneOf.js index 09ed526..408cef9 100644 --- a/src/error-handlers/oneOf.js +++ b/src/error-handlers/oneOf.js @@ -51,7 +51,7 @@ const oneOfErrorHandler = async (normalizedErrors, instance, localization) => { const isObject = Instance.typeOf(instance) === "object"; const instanceProps = isObject - ? Pact.collectSet(Pact.map((keyNode) => /** @type {string} */ (Instance.value(keyNode)), Instance.keys(instance))) + ? Pact.collectSet(Pact.pipe(Instance.keys(instance), Pact.map((keyNode) => /** @type {string} */ (Instance.value(keyNode))))) : undefined; const prefix = `${instanceLocation}/`; @@ -63,17 +63,13 @@ const oneOfErrorHandler = async (normalizedErrors, instance, localization) => { } if (isObject) { - const declaredProps = Pact.map( - (loc) => /** @type {string} */ (Pact.head(JsonPointer.pointerSegments(loc.slice(prefix.length - 1)))), - Pact.filter((loc) => loc.startsWith(prefix), Object.keys(alternative)) - ); - - let hasDeclaredProps = false; - const hasMatchingProp = Pact.some((prop) => { - hasDeclaredProps = true; - return /** @type {Set} */ (instanceProps).has(prop); - }, declaredProps); - if (hasDeclaredProps && !hasMatchingProp) { + const declaredProps = Pact.collectSet(Pact.pipe( + Object.keys(alternative), + Pact.filter((loc) => loc.startsWith(prefix)), + Pact.map((loc) => /** @type {string} */ (Pact.head(JsonPointer.pointerSegments(loc.slice(prefix.length - 1))))) + )); + + if (declaredProps.size > 0 && !Pact.some((prop) => declaredProps.has(prop), /** @type {Set} */ (instanceProps))) { continue; } } @@ -85,26 +81,37 @@ const oneOfErrorHandler = async (normalizedErrors, instance, localization) => { filtered = oneOf; } + /** @type ErrorObject[][] */ + const alternatives = []; + if (isObject) { const discriminators = Pact.collectSet( - Pact.filter( - (prop) => Pact.some((alternative) => propertyPasses(alternative[JsonPointer.append(prop, instanceLocation)]), filtered), - /** @type {Set} */ (instanceProps) - ) + /** @type {Iterable} */ (Pact.pipe( + filtered, + Pact.map((alternative) => Pact.pipe( + /** @type {Set} */ (instanceProps), + Pact.filter((prop) => propertyPasses(alternative[JsonPointer.append(prop, instanceLocation)])) + )), + Pact.flatten + )) ); - const afterRule2 = Pact.collectArray(Pact.filter((alternative) => !Pact.some((prop) => !propertyPasses(alternative[JsonPointer.append(prop, instanceLocation)]), discriminators), filtered)); - - if (afterRule2.length > 0) { - filtered = afterRule2; + for (const alternative of filtered) { + if (!Pact.some((prop) => !propertyPasses(alternative[JsonPointer.append(prop, instanceLocation)]), discriminators)) { + const alternativeErrors = await getErrors(alternative, instance, localization); + if (alternativeErrors.length) { + alternatives.push(alternativeErrors); + } + } } } - const alternatives = []; - for (const alternative of filtered) { - const alternativeErrors = await getErrors(alternative, instance, localization); - if (alternativeErrors.length) { - alternatives.push(alternativeErrors); + if (alternatives.length === 0) { + for (const alternative of filtered) { + const alternativeErrors = await getErrors(alternative, instance, localization); + if (alternativeErrors.length) { + alternatives.push(alternativeErrors); + } } } @@ -112,15 +119,15 @@ const oneOfErrorHandler = async (normalizedErrors, instance, localization) => { errors.push(...alternatives[0]); } else { /** @type ErrorObject */ - const alternativeErrors = { + const error = { message: localization.getOneOfErrorMessage(0), instanceLocation, schemaLocations: [schemaLocation] }; if (alternatives.length) { - alternativeErrors.alternatives = alternatives; + error.alternatives = alternatives; } - errors.push(alternativeErrors); + errors.push(error); } } From 38936d24f7e20736ebe2ed2e7a853c6061f5d097 Mon Sep 17 00:00:00 2001 From: Jason Desrosiers Date: Wed, 1 Apr 2026 11:27:23 -0700 Subject: [PATCH 07/11] Cleanup pipeline usage --- src/error-handlers/anyOf.js | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/error-handlers/anyOf.js b/src/error-handlers/anyOf.js index 92132bd..e0e86da 100644 --- a/src/error-handlers/anyOf.js +++ b/src/error-handlers/anyOf.js @@ -17,13 +17,16 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => { if (typeof anyOf === "boolean") { continue; } + const instanceLocation = Instance.uri(instance); let filtered = anyOf; const isObject = Instance.typeOf(instance) === "object"; - const instanceProps = isObject - ? Pact.collectSet(Pact.pipe(Instance.keys(instance), Pact.map((keyNode) => /** @type {string} */ (Instance.value(keyNode))))) - : undefined; + const instanceProps = Pact.pipe( + Instance.keys(instance), + Pact.map((keyNode) => /** @type {string} */ (Instance.value(keyNode))), + Pact.collectSet + ); const prefix = `${instanceLocation}/`; filtered = []; @@ -34,11 +37,12 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => { } if (isObject) { - const declaredProps = Pact.collectSet(Pact.pipe( + const declaredProps = Pact.pipe( Object.keys(alternative), Pact.filter((loc) => loc.startsWith(prefix)), - Pact.map((loc) => /** @type {string} */ (Pact.head(JsonPointer.pointerSegments(loc.slice(prefix.length - 1))))) - )); + Pact.map((loc) => /** @type {string} */ (Pact.head(JsonPointer.pointerSegments(loc.slice(prefix.length - 1))))), + Pact.collectSet + ); if (declaredProps.size > 0 && !Pact.some((prop) => declaredProps.has(prop), /** @type {Set} */ (instanceProps))) { continue; @@ -56,15 +60,13 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => { const alternatives = []; if (isObject) { - const discriminators = Pact.collectSet( - /** @type {Iterable} */ (Pact.pipe( - filtered, - Pact.map((alternative) => Pact.pipe( - /** @type {Set} */ (instanceProps), - Pact.filter((prop) => propertyPasses(alternative[JsonPointer.append(prop, instanceLocation)])) - )), - Pact.flatten - )) + const discriminators = Pact.pipe( + instanceProps, + Pact.filter((prop) => { + const propLocation = JsonPointer.append(prop, instanceLocation); + return Pact.some((alternative) => propertyPasses(alternative[propLocation]), filtered); + }), + Pact.collectSet ); for (const alternative of filtered) { From f7934f9ecfe2ca1ac7c4730e600ea5325e9c6dd6 Mon Sep 17 00:00:00 2001 From: Jason Desrosiers Date: Wed, 1 Apr 2026 11:43:34 -0700 Subject: [PATCH 08/11] Move discriminator filtering into the main loop --- src/error-handlers/anyOf.js | 39 ++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/src/error-handlers/anyOf.js b/src/error-handlers/anyOf.js index e0e86da..8d3db58 100644 --- a/src/error-handlers/anyOf.js +++ b/src/error-handlers/anyOf.js @@ -21,22 +21,32 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => { const instanceLocation = Instance.uri(instance); let filtered = anyOf; - const isObject = Instance.typeOf(instance) === "object"; const instanceProps = Pact.pipe( Instance.keys(instance), Pact.map((keyNode) => /** @type {string} */ (Instance.value(keyNode))), Pact.collectSet ); + + const discriminators = Pact.pipe( + instanceProps, + Pact.filter((prop) => { + const propLocation = JsonPointer.append(prop, instanceLocation); + return Pact.some((alternative) => propertyPasses(alternative[propLocation]), anyOf); + }), + Pact.collectSet + ); + const prefix = `${instanceLocation}/`; filtered = []; for (const alternative of anyOf) { + // Filter alternatives whose declared type doesn't match the instance type const typeResults = alternative[instanceLocation]?.["https://json-schema.org/keyword/type"]; if (typeResults && !Object.values(typeResults).every((isValid) => isValid)) { continue; } - if (isObject) { + if (Instance.typeOf(instance) === "object") { const declaredProps = Pact.pipe( Object.keys(alternative), Pact.filter((loc) => loc.startsWith(prefix)), @@ -44,7 +54,13 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => { Pact.collectSet ); - if (declaredProps.size > 0 && !Pact.some((prop) => declaredProps.has(prop), /** @type {Set} */ (instanceProps))) { + // Filter alternative if it has no declared properties in common with the instance + if (!Pact.some((prop) => declaredProps.has(prop), instanceProps)) { + continue; + } + + // Filter alternative if it has failing properties that are decalred and passing in another alternative + if (Pact.some((prop) => !propertyPasses(alternative[JsonPointer.append(prop, instanceLocation)]), discriminators)) { continue; } } @@ -59,23 +75,6 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => { /** @type ErrorObject[][] */ const alternatives = []; - if (isObject) { - const discriminators = Pact.pipe( - instanceProps, - Pact.filter((prop) => { - const propLocation = JsonPointer.append(prop, instanceLocation); - return Pact.some((alternative) => propertyPasses(alternative[propLocation]), filtered); - }), - Pact.collectSet - ); - - for (const alternative of filtered) { - if (!Pact.some((prop) => !propertyPasses(alternative[JsonPointer.append(prop, instanceLocation)]), discriminators)) { - alternatives.push(await getErrors(alternative, instance, localization)); - } - } - } - if (alternatives.length === 0) { for (const alternative of filtered) { alternatives.push(await getErrors(alternative, instance, localization)); From 053be940324b565f9f2b399291e7fe62dd3f887b Mon Sep 17 00:00:00 2001 From: animesh sahoo Date: Mon, 6 Apr 2026 17:18:42 +0530 Subject: [PATCH 09/11] apply discussed changes --- src/error-handlers/anyOf.js | 21 +++++---------- src/error-handlers/oneOf.js | 54 +++++++++++++++---------------------- 2 files changed, 28 insertions(+), 47 deletions(-) diff --git a/src/error-handlers/anyOf.js b/src/error-handlers/anyOf.js index 8d3db58..1110b08 100644 --- a/src/error-handlers/anyOf.js +++ b/src/error-handlers/anyOf.js @@ -19,26 +19,20 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => { } const instanceLocation = Instance.uri(instance); - let filtered = anyOf; const instanceProps = Pact.pipe( Instance.keys(instance), - Pact.map((keyNode) => /** @type {string} */ (Instance.value(keyNode))), + Pact.map((keyNode) => JsonPointer.append(/** @type {string} */ (Instance.value(keyNode)), instanceLocation)), Pact.collectSet ); const discriminators = Pact.pipe( instanceProps, - Pact.filter((prop) => { - const propLocation = JsonPointer.append(prop, instanceLocation); - return Pact.some((alternative) => propertyPasses(alternative[propLocation]), anyOf); - }), + Pact.filter((propLocation) => Pact.some((alternative) => propertyPasses(alternative[propLocation]), anyOf)), Pact.collectSet ); - const prefix = `${instanceLocation}/`; - - filtered = []; + let filtered = []; for (const alternative of anyOf) { // Filter alternatives whose declared type doesn't match the instance type const typeResults = alternative[instanceLocation]?.["https://json-schema.org/keyword/type"]; @@ -49,18 +43,17 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => { if (Instance.typeOf(instance) === "object") { const declaredProps = Pact.pipe( Object.keys(alternative), - Pact.filter((loc) => loc.startsWith(prefix)), - Pact.map((loc) => /** @type {string} */ (Pact.head(JsonPointer.pointerSegments(loc.slice(prefix.length - 1))))), + Pact.filter((loc) => instanceProps.has(loc)), Pact.collectSet ); // Filter alternative if it has no declared properties in common with the instance - if (!Pact.some((prop) => declaredProps.has(prop), instanceProps)) { + if (!declaredProps.size) { continue; } - // Filter alternative if it has failing properties that are decalred and passing in another alternative - if (Pact.some((prop) => !propertyPasses(alternative[JsonPointer.append(prop, instanceLocation)]), discriminators)) { + // Filter alternative if it has failing properties that are declared and passing in another alternative + if (Pact.some((propLocation) => !propertyPasses(alternative[propLocation]), discriminators)) { continue; } } diff --git a/src/error-handlers/oneOf.js b/src/error-handlers/oneOf.js index 408cef9..da237a0 100644 --- a/src/error-handlers/oneOf.js +++ b/src/error-handlers/oneOf.js @@ -49,11 +49,17 @@ const oneOfErrorHandler = async (normalizedErrors, instance, localization) => { let filtered = oneOf; - const isObject = Instance.typeOf(instance) === "object"; - const instanceProps = isObject - ? Pact.collectSet(Pact.pipe(Instance.keys(instance), Pact.map((keyNode) => /** @type {string} */ (Instance.value(keyNode))))) - : undefined; - const prefix = `${instanceLocation}/`; + const instanceProps = Pact.pipe( + Instance.keys(instance), + Pact.map((keyNode) => JsonPointer.append(/** @type {string} */ (Instance.value(keyNode)), instanceLocation)), + Pact.collectSet + ); + + const discriminators = Pact.pipe( + instanceProps, + Pact.filter((propLocation) => Pact.some((alternative) => propertyPasses(alternative[propLocation]), oneOf)), + Pact.collectSet + ); filtered = []; for (const alternative of oneOf) { @@ -62,14 +68,18 @@ const oneOfErrorHandler = async (normalizedErrors, instance, localization) => { continue; } - if (isObject) { - const declaredProps = Pact.collectSet(Pact.pipe( + if (Instance.typeOf(instance) === "object") { + const declaredProps = Pact.pipe( Object.keys(alternative), - Pact.filter((loc) => loc.startsWith(prefix)), - Pact.map((loc) => /** @type {string} */ (Pact.head(JsonPointer.pointerSegments(loc.slice(prefix.length - 1))))) - )); + Pact.filter((loc) => instanceProps.has(loc)), + Pact.collectSet + ); - if (declaredProps.size > 0 && !Pact.some((prop) => declaredProps.has(prop), /** @type {Set} */ (instanceProps))) { + if (!declaredProps.size) { + continue; + } + + if (Pact.some((propLocation) => !propertyPasses(alternative[propLocation]), discriminators)) { continue; } } @@ -84,28 +94,6 @@ const oneOfErrorHandler = async (normalizedErrors, instance, localization) => { /** @type ErrorObject[][] */ const alternatives = []; - if (isObject) { - const discriminators = Pact.collectSet( - /** @type {Iterable} */ (Pact.pipe( - filtered, - Pact.map((alternative) => Pact.pipe( - /** @type {Set} */ (instanceProps), - Pact.filter((prop) => propertyPasses(alternative[JsonPointer.append(prop, instanceLocation)])) - )), - Pact.flatten - )) - ); - - for (const alternative of filtered) { - if (!Pact.some((prop) => !propertyPasses(alternative[JsonPointer.append(prop, instanceLocation)]), discriminators)) { - const alternativeErrors = await getErrors(alternative, instance, localization); - if (alternativeErrors.length) { - alternatives.push(alternativeErrors); - } - } - } - } - if (alternatives.length === 0) { for (const alternative of filtered) { const alternativeErrors = await getErrors(alternative, instance, localization); From a5767e6622ed2e9db7df8bf8ceda8e9f1eb6ba62 Mon Sep 17 00:00:00 2001 From: Jason Desrosiers Date: Tue, 7 Apr 2026 16:55:55 -0700 Subject: [PATCH 10/11] Using property locations allowed us to simplify even more! --- src/error-handlers/anyOf.js | 66 ++++++++++---------- src/error-handlers/oneOf.js | 120 ++++++++++++++---------------------- 2 files changed, 77 insertions(+), 109 deletions(-) diff --git a/src/error-handlers/anyOf.js b/src/error-handlers/anyOf.js index 1110b08..fd20ce0 100644 --- a/src/error-handlers/anyOf.js +++ b/src/error-handlers/anyOf.js @@ -1,10 +1,9 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; -import * as JsonPointer from "@hyperjump/json-pointer"; import * as Pact from "@hyperjump/pact"; import { getErrors } from "../json-schema-errors.js"; /** - * @import { ErrorHandler, ErrorObject, NormalizedOutput } from "../index.d.ts" + * @import { ErrorHandler, ErrorObject, InstanceOutput } from "../index.d.ts" */ /** @type ErrorHandler */ @@ -18,21 +17,20 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => { continue; } - const instanceLocation = Instance.uri(instance); - - const instanceProps = Pact.pipe( - Instance.keys(instance), - Pact.map((keyNode) => JsonPointer.append(/** @type {string} */ (Instance.value(keyNode)), instanceLocation)), - Pact.collectSet + const propertyLocations = Pact.pipe( + Instance.values(instance), + Pact.map(Instance.uri), + Pact.collectArray ); - const discriminators = Pact.pipe( - instanceProps, - Pact.filter((propLocation) => Pact.some((alternative) => propertyPasses(alternative[propLocation]), anyOf)), - Pact.collectSet - ); + const discriminators = propertyLocations.filter((propertyLocation) => { + return anyOf.some((alternative) => isPassingProperty(alternative[propertyLocation])); + }); + + /** @type ErrorObject[][] */ + const alternatives = []; + const instanceLocation = Instance.uri(instance); - let filtered = []; for (const alternative of anyOf) { // Filter alternatives whose declared type doesn't match the instance type const typeResults = alternative[instanceLocation]?.["https://json-schema.org/keyword/type"]; @@ -41,35 +39,24 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => { } if (Instance.typeOf(instance) === "object") { - const declaredProps = Pact.pipe( - Object.keys(alternative), - Pact.filter((loc) => instanceProps.has(loc)), - Pact.collectSet - ); - // Filter alternative if it has no declared properties in common with the instance - if (!declaredProps.size) { + if (!propertyLocations.some((propertyLocation) => propertyLocation in alternative)) { continue; } // Filter alternative if it has failing properties that are declared and passing in another alternative - if (Pact.some((propLocation) => !propertyPasses(alternative[propLocation]), discriminators)) { + if (discriminators.some((propertyLocation) => !isPassingProperty(alternative[propertyLocation]))) { continue; } } - filtered.push(alternative); - } - - if (filtered.length === 0) { - filtered = anyOf; + // The alternative passed all the filters + alternatives.push(await getErrors(alternative, instance, localization)); } - /** @type ErrorObject[][] */ - const alternatives = []; - + // If all alternatives were filtered out, default to returning all of them if (alternatives.length === 0) { - for (const alternative of filtered) { + for (const alternative of anyOf) { alternatives.push(await getErrors(alternative, instance, localization)); } } @@ -89,12 +76,21 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => { return errors; }; -/** @type (propOutput: NormalizedOutput[string] | undefined) => boolean */ -const propertyPasses = (propOutput) => { - if (!propOutput) { +/** @type (alternative: InstanceOutput | undefined) => boolean */ +const isPassingProperty = (propertyOutput) => { + if (!propertyOutput) { return false; } - return Object.values(propOutput).every((keywordResults) => Object.values(keywordResults).every((v) => v === true)); + + for (const keywordUri in propertyOutput) { + for (const schemaLocation in propertyOutput[keywordUri]) { + if (propertyOutput[keywordUri][schemaLocation] !== true) { + return false; + } + } + } + + return true; }; export default anyOfErrorHandler; diff --git a/src/error-handlers/oneOf.js b/src/error-handlers/oneOf.js index da237a0..63aa4ef 100644 --- a/src/error-handlers/oneOf.js +++ b/src/error-handlers/oneOf.js @@ -1,10 +1,9 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; -import * as JsonPointer from "@hyperjump/json-pointer"; import * as Pact from "@hyperjump/pact"; import { getErrors } from "../json-schema-errors.js"; /** - * @import { ErrorHandler, ErrorObject, NormalizedOutput } from "../index.d.ts" + * @import { ErrorHandler, ErrorObject, InstanceOutput } from "../index.d.ts" */ /** @type ErrorHandler */ @@ -18,116 +17,89 @@ const oneOfErrorHandler = async (normalizedErrors, instance, localization) => { continue; } - const instanceLocation = Instance.uri(instance); - - let matchCount = 0; - /** @type ErrorObject[][] */ - const failingAlternatives = []; - - for (const alternative of oneOf) { - const alternativeErrors = await getErrors(alternative, instance, localization); - if (alternativeErrors.length) { - failingAlternatives.push(alternativeErrors); - } else { - matchCount++; - } - } - - if (matchCount > 1) { - /** @type ErrorObject */ - const error = { - message: localization.getOneOfErrorMessage(matchCount), - instanceLocation, - schemaLocations: [schemaLocation] - }; - if (failingAlternatives.length) { - error.alternatives = failingAlternatives; - } - errors.push(error); - continue; - } - - let filtered = oneOf; - - const instanceProps = Pact.pipe( - Instance.keys(instance), - Pact.map((keyNode) => JsonPointer.append(/** @type {string} */ (Instance.value(keyNode)), instanceLocation)), - Pact.collectSet + const propertyLocations = Pact.pipe( + Instance.values(instance), + Pact.map(Instance.uri), + Pact.collectArray ); - const discriminators = Pact.pipe( - instanceProps, - Pact.filter((propLocation) => Pact.some((alternative) => propertyPasses(alternative[propLocation]), oneOf)), - Pact.collectSet - ); + const discriminators = propertyLocations.filter((propertyLocation) => { + return oneOf.some((alternative) => isPassingProperty(alternative[propertyLocation])); + }); + + const alternatives = []; + const instanceLocation = Instance.uri(instance); + let matchCount = 0; - filtered = []; for (const alternative of oneOf) { + // Filter alternatives whose declared type doesn't match the instance type const typeResults = alternative[instanceLocation]?.["https://json-schema.org/keyword/type"]; if (typeResults && !Object.values(typeResults).every((isValid) => isValid)) { continue; } if (Instance.typeOf(instance) === "object") { - const declaredProps = Pact.pipe( - Object.keys(alternative), - Pact.filter((loc) => instanceProps.has(loc)), - Pact.collectSet - ); - - if (!declaredProps.size) { + // Filter alternative if it has no declared properties in common with the instance + if (!propertyLocations.some((propertyLocation) => propertyLocation in alternative)) { continue; } - if (Pact.some((propLocation) => !propertyPasses(alternative[propLocation]), discriminators)) { + // Filter alternative if it has failing properties that are declared and passing in another alternative + if (discriminators.some((propertyLocation) => !isPassingProperty(alternative[propertyLocation]))) { continue; } } - filtered.push(alternative); - } - - if (filtered.length === 0) { - filtered = oneOf; + // The alternative passed all the filters + const alternativeErrors = await getErrors(alternative, instance, localization); + if (alternativeErrors.length) { + alternatives.push(alternativeErrors); + } else { + matchCount++; + } } - /** @type ErrorObject[][] */ - const alternatives = []; - - if (alternatives.length === 0) { - for (const alternative of filtered) { + if (matchCount === 0 && alternatives.length === 0) { + for (const alternative of oneOf) { const alternativeErrors = await getErrors(alternative, instance, localization); - if (alternativeErrors.length) { - alternatives.push(alternativeErrors); - } + alternatives.push(alternativeErrors); } } - if (alternatives.length === 1) { + if (alternatives.length === 1 && matchCount === 0) { errors.push(...alternatives[0]); } else { /** @type ErrorObject */ - const error = { - message: localization.getOneOfErrorMessage(0), - instanceLocation, + const alternativeErrors = { + message: localization.getOneOfErrorMessage(matchCount), + instanceLocation: Instance.uri(instance), schemaLocations: [schemaLocation] }; if (alternatives.length) { - error.alternatives = alternatives; + alternativeErrors.alternatives = alternatives; } - errors.push(error); + errors.push(alternativeErrors); } } return errors; }; -/** @type (propOutput: NormalizedOutput[string] | undefined) => boolean */ -const propertyPasses = (propOutput) => { - if (!propOutput) { +/** @type (alternative: InstanceOutput | undefined) => boolean */ +const isPassingProperty = (propertyOutput) => { + if (!propertyOutput) { return false; } - return Object.values(propOutput).every((keywordResults) => Object.values(keywordResults).every((v) => v === true)); + + for (const keywordUri in propertyOutput) { + for (const schemaLocation in propertyOutput[keywordUri]) { + if (propertyOutput[keywordUri][schemaLocation] !== true) { + return false; + } + } + } + + return true; }; export default oneOfErrorHandler; From 68f1e168af5c1ae13db9b9100a35d162f7e170a2 Mon Sep 17 00:00:00 2001 From: Jason Desrosiers Date: Tue, 7 Apr 2026 17:29:10 -0700 Subject: [PATCH 11/11] Add a couple more tests --- src/test-suite/tests/anyOf.json | 129 +++++++++++++++++++++++++++++- src/test-suite/tests/oneOf.json | 134 +++++++++++++++++++++++++++++++- 2 files changed, 255 insertions(+), 8 deletions(-) diff --git a/src/test-suite/tests/anyOf.json b/src/test-suite/tests/anyOf.json index b377cf9..4f80ffe 100644 --- a/src/test-suite/tests/anyOf.json +++ b/src/test-suite/tests/anyOf.json @@ -20,7 +20,7 @@ { "messageId": "type-message", "messageParams": { - "expectedTypes": "string" + "expectedTypes": { "or": ["string"] } }, "instanceLocation": "#", "schemaLocations": ["#/anyOf/0/type"] @@ -30,7 +30,7 @@ { "messageId": "type-message", "messageParams": { - "expectedTypes": "number" + "expectedTypes": { "or": ["number"] } }, "instanceLocation": "#", "schemaLocations": ["#/anyOf/1/type"] @@ -254,6 +254,91 @@ } ] }, + { + "description": "alternatives with no declared properties matching the instance are filtered out", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "a": { "type": "string" } + }, + "required": ["a"] + }, + { + "type": "object", + "properties": { + "b": { "type": "string" } + }, + "required": ["b"] + } + ] + }, + "instance": { "a": 42 }, + "errors": [ + { + "messageId": "type-message", + "messageParams": { + "expectedTypes": { "or": ["string"] } + }, + "instanceLocation": "#/a", + "schemaLocations": ["#/anyOf/0/properties/a/type"] + } + ] + }, + { + "description": "None of the instance properties are declared in any of the alternatives", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "a": { "type": "string" } + }, + "required": ["a"] + }, + { + "type": "object", + "properties": { + "b": { "type": "string" } + }, + "required": ["b"] + } + ] + }, + "instance": { "c": 42 }, + "errors": [ + { + "messageId": "anyOf-message", + "alternatives": [ + [ + { + "messageId": "required-message", + "messageParams": { + "required": { "and": ["a"] }, + "count": 1 + }, + "instanceLocation": "#", + "schemaLocations": ["#/anyOf/0/required"] + } + ], + [ + { + "messageId": "required-message", + "messageParams": { + "required": { "and": ["b"] }, + "count": 1 + }, + "instanceLocation": "#", + "schemaLocations": ["#/anyOf/1/required"] + } + ] + ], + "instanceLocation": "#", + "schemaLocations": ["#/anyOf"] + } + ] + }, { "description": "anyOf object alternatives keep only one branch when only one branch has a passing instance property", "compatibility": "6", @@ -282,7 +367,7 @@ { "messageId": "type-message", "messageParams": { - "expectedTypes": "number" + "expectedTypes": { "or": ["number"] } }, "instanceLocation": "#/b", "schemaLocations": ["#/anyOf/1/properties/b/type"] @@ -326,7 +411,7 @@ { "messageId": "type-message", "messageParams": { - "expectedTypes": "string" + "expectedTypes": { "or": ["string"] } }, "instanceLocation": "#/x", "schemaLocations": ["#/anyOf/0/properties/x/type"] @@ -432,6 +517,42 @@ "schemaLocations": ["#/anyOf"] } ] + }, + { + "description": "Compound discriminators", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "foo": { "enum": ["a"] }, + "bar": { "enum": ["b"] }, + "a": { "type": "string" } + }, + "required": ["foo", "bar"] + }, + { + "type": "object", + "properties": { + "foo": { "enum": ["a"] }, + "bar": { "enum": ["c"] }, + "b": { "type": "string" } + }, + "required": ["foo", "bar"] + } + ] + }, + "instance": { "foo": "a", "bar": "c", "b": 42 }, + "errors": [ + { + "messageId": "type-message", + "messageParams": { + "expectedTypes": { "or": ["string"] } + }, + "instanceLocation": "#/b", + "schemaLocations": ["#/anyOf/1/properties/b/type"] + } + ] } ] } diff --git a/src/test-suite/tests/oneOf.json b/src/test-suite/tests/oneOf.json index f2038ac..c6466d1 100644 --- a/src/test-suite/tests/oneOf.json +++ b/src/test-suite/tests/oneOf.json @@ -15,7 +15,9 @@ "errors": [ { "messageId": "oneOf-message", - "messageParams": { "matchCount": 0 }, + "messageParams": { + "matchCount": 0 + }, "alternatives": [ [ { @@ -322,6 +324,94 @@ } ] }, + { + "description": "alternatives with no declared properties matching the instance are filtered out", + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "a": { "type": "string" } + }, + "required": ["a"] + }, + { + "type": "object", + "properties": { + "b": { "type": "string" } + }, + "required": ["b"] + } + ] + }, + "instance": { "a": 42 }, + "errors": [ + { + "messageId": "type-message", + "messageParams": { + "expectedTypes": { "or": ["string"] } + }, + "instanceLocation": "#/a", + "schemaLocations": ["#/oneOf/0/properties/a/type"] + } + ] + }, + { + "description": "None of the instance properties are declared in any of the alternatives", + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "a": { "type": "string" } + }, + "required": ["a"] + }, + { + "type": "object", + "properties": { + "b": { "type": "string" } + }, + "required": ["b"] + } + ] + }, + "instance": { "c": 42 }, + "errors": [ + { + "messageId": "oneOf-message", + "messageParams": { + "matchCount": 0 + }, + "alternatives": [ + [ + { + "messageId": "required-message", + "messageParams": { + "required": { "and": ["a"] }, + "count": 1 + }, + "instanceLocation": "#", + "schemaLocations": ["#/oneOf/0/required"] + } + ], + [ + { + "messageId": "required-message", + "messageParams": { + "required": { "and": ["b"] }, + "count": 1 + }, + "instanceLocation": "#", + "schemaLocations": ["#/oneOf/1/required"] + } + ] + ], + "instanceLocation": "#", + "schemaLocations": ["#/oneOf"] + } + ] + }, { "description": "oneOf object alternatives keep only one branch when only one branch has a passing instance property", "compatibility": "6", @@ -350,7 +440,7 @@ { "messageId": "type-message", "messageParams": { - "expectedTypes": "number" + "expectedTypes": { "or": ["number"] } }, "instanceLocation": "#/b", "schemaLocations": ["#/oneOf/1/properties/b/type"] @@ -383,7 +473,7 @@ { "messageId": "type-message", "messageParams": { - "expectedTypes": "string" + "expectedTypes": { "or": ["string"] } }, "instanceLocation": "#/a", "schemaLocations": ["#/oneOf/0/properties/a/type"] @@ -485,7 +575,7 @@ { "messageId": "type-message", "messageParams": { - "expectedTypes": "string" + "expectedTypes": { "or": ["string"] } }, "instanceLocation": "#/x", "schemaLocations": ["#/oneOf/0/properties/x/type"] @@ -506,6 +596,42 @@ "schemaLocations": ["#/oneOf"] } ] + }, + { + "description": "Compound discriminators", + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "foo": { "enum": ["a"] }, + "bar": { "enum": ["b"] }, + "a": { "type": "string" } + }, + "required": ["foo", "bar"] + }, + { + "type": "object", + "properties": { + "foo": { "enum": ["a"] }, + "bar": { "enum": ["c"] }, + "b": { "type": "string" } + }, + "required": ["foo", "bar"] + } + ] + }, + "instance": { "foo": "a", "bar": "c", "b": 42 }, + "errors": [ + { + "messageId": "type-message", + "messageParams": { + "expectedTypes": { "or": ["string"] } + }, + "instanceLocation": "#/b", + "schemaLocations": ["#/oneOf/1/properties/b/type"] + } + ] } ] }