diff --git a/src/error-handlers/anyOf.js b/src/error-handlers/anyOf.js index fc83451..cab2475 100644 --- a/src/error-handlers/anyOf.js +++ b/src/error-handlers/anyOf.js @@ -17,16 +17,33 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => { } const alternatives = []; + const instanceLocation = Instance.uri(instance); + for (const alternative of anyOf) { - alternatives.push(await getErrors(alternative, instance, localization)); + const typeErrors = alternative[instanceLocation]["https://json-schema.org/keyword/type"]; + const match = !typeErrors || Object.values(typeErrors).every((isValid) => isValid); + + if (match) { + alternatives.push(await getErrors(alternative, instance, localization)); + } } - errors.push({ - message: localization.getAnyOfErrorMessage(), - alternatives: alternatives, - instanceLocation: Instance.uri(instance), - schemaLocations: [schemaLocation] - }); + if (alternatives.length === 0) { + for (const alternative of anyOf) { + 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), + schemaLocations: [schemaLocation] + }); + } } return errors; diff --git a/src/error-handlers/oneOf.js b/src/error-handlers/oneOf.js index 56e1777..82b15d7 100644 --- a/src/error-handlers/oneOf.js +++ b/src/error-handlers/oneOf.js @@ -17,26 +17,44 @@ const oneOfErrorHandler = async (normalizedErrors, instance, localization) => { } const alternatives = []; + const instanceLocation = Instance.uri(instance); let matchCount = 0; + for (const alternative of oneOf) { - const alternativeErrors = await getErrors(alternative, instance, localization); - if (alternativeErrors.length) { + 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++; + } + } + } + + if (matchCount === 0 && alternatives.length === 0) { + for (const alternative of oneOf) { + const alternativeErrors = await getErrors(alternative, instance, localization); alternatives.push(alternativeErrors); - } else { - matchCount++; } } - /** @type ErrorObject */ - const alternativeErrors = { - message: localization.getOneOfErrorMessage(matchCount), - instanceLocation: Instance.uri(instance), - schemaLocations: [schemaLocation] - }; - if (alternatives.length) { - alternativeErrors.alternatives = alternatives; + if (alternatives.length === 1 && matchCount === 0) { + errors.push(...alternatives[0]); + } else { + /** @type ErrorObject */ + const alternativeErrors = { + message: localization.getOneOfErrorMessage(matchCount), + instanceLocation: Instance.uri(instance), + schemaLocations: [schemaLocation] + }; + if (alternatives.length) { + alternativeErrors.alternatives = alternatives; + } + errors.push(alternativeErrors); } - errors.push(alternativeErrors); } return errors; diff --git a/src/test-suite/tests/anyOf.json b/src/test-suite/tests/anyOf.json index 5460df0..93ee92e 100644 --- a/src/test-suite/tests/anyOf.json +++ b/src/test-suite/tests/anyOf.json @@ -52,6 +52,161 @@ }, "instance": 42, "errors": [] + }, + { + "description": "anyOf single type match flattens errors (removes anyOf error message)", + "schema": { + "anyOf": [ + { + "type": "string", + "maxLength": 2 + }, + { + "type": "number", + "minimum": 2 + } + ] + }, + "instance": "foo", + "errors": [ + { + "messageId": "maxLength-message", + "messageParams": { + "maxLength": 2 + }, + "instanceLocation": "#", + "schemaLocations": ["#/anyOf/0/maxLength"] + } + ] + }, + { + "description": "none of the alternatives match instance type, hence no filtering of alternatives", + "schema": { + "anyOf": [ + { + "type": "string", + "maxLength": 2 + }, + { + "type": "number", + "minimum": 2 + } + ] + }, + "instance": true, + "errors": [ + { + "messageId": "anyOf-message", + "alternatives": [ + [ + { + "messageId": "type-message", + "messageParams": { + "expectedTypes": { "or": ["string"] } + }, + "instanceLocation": "#", + "schemaLocations": ["#/anyOf/0/type"] + } + ], + [ + { + "messageId": "type-message", + "messageParams": { + "expectedTypes": { "or": ["number"] } + }, + "instanceLocation": "#", + "schemaLocations": ["#/anyOf/1/type"] + } + ] + ], + "instanceLocation": "#", + "schemaLocations": ["#/anyOf"] + } + ] + }, + { + "description": "anyOf with multiple matching types does not collapse distinct errors", + "schema": { + "anyOf": [ + { + "type": "string", + "pattern": "^[0-9]+$" + }, + { + "type": "string", + "format": "email" + }, + { + "type": "number", + "minimum": 2 + } + ] + }, + "instance": "hello", + "errors": [ + { + "messageId": "anyOf-message", + "alternatives": [ + [ + { + "messageId": "pattern-message", + "messageParams": { + "pattern": "^[0-9]+$" + }, + "instanceLocation": "#", + "schemaLocations": ["#/anyOf/0/pattern"] + } + ], + [ + { + "messageId": "format-message", + "messageParams": { + "format": "email" + }, + "instanceLocation": "#", + "schemaLocations": ["#/anyOf/1/format"] + } + ] + ], + "instanceLocation": "#", + "schemaLocations": ["#/anyOf"] + } + ] + }, + { + "description": "anyOf with $ref alternatives filters by type", + "compatibility": "2019", + "schema": { + "$defs": { + "bar": { + "type": "string", + "maxLength": 2 + }, + "baz": { + "type": "number", + "minimum": 2 + } + }, + "anyOf": [ + { + "$ref": "#/$defs/bar" + }, + { + "$ref": "#/$defs/baz" + } + ] + }, + "instance": "foo", + "errors": [ + { + "messageId": "maxLength-message", + "messageParams": { + "maxLength": 2 + }, + "instanceLocation": "#", + "schemaLocations": ["#/$defs/bar/maxLength"] + } + ] } ] } diff --git a/src/test-suite/tests/oneOf.json b/src/test-suite/tests/oneOf.json index 658af9d..94743ef 100644 --- a/src/test-suite/tests/oneOf.json +++ b/src/test-suite/tests/oneOf.json @@ -63,6 +63,42 @@ } ] }, + { + "description": "oneOf more than one match with an additional failing alternative", + "schema": { + "oneOf": [ + { "type": "integer" }, + { "type": "number" }, + { + "type": "number", + "minimum": 5 + } + ] + }, + "instance": 1, + "errors": [ + { + "messageId": "oneOf-message", + "messageParams": { + "matchCount": 2 + }, + "alternatives": [ + [ + { + "messageId": "minimum-message", + "messageParams": { + "minimum": 5 + }, + "instanceLocation": "#", + "schemaLocations": ["#/oneOf/2/minimum"] + } + ] + ], + "instanceLocation": "#", + "schemaLocations": ["#/oneOf"] + } + ] + }, { "description": "oneOf pass", "schema": { @@ -73,6 +109,169 @@ }, "instance": 42, "errors": [] + }, + { + "description": "oneOf single type match flattens errors (removes oneOf error message)", + "schema": { + "oneOf": [ + { + "type": "string", + "maxLength": 2 + }, + { + "type": "number", + "minimum": 2 + } + ] + }, + "instance": "foo", + "errors": [ + { + "messageId": "maxLength-message", + "messageParams": { + "maxLength": 2 + }, + "instanceLocation": "#", + "schemaLocations": ["#/oneOf/0/maxLength"] + } + ] + }, + { + "description": "oneOf with multiple matching types does not collapse distinct errors", + "schema": { + "oneOf": [ + { + "type": "string", + "pattern": "^[0-9]+$" + }, + { + "type": "string", + "format": "email" + }, + { + "type": "number", + "minimum": 2 + } + ] + }, + "instance": "hello", + "errors": [ + { + "messageId": "oneOf-message", + "messageParams": { + "matchCount": 0 + }, + "alternatives": [ + [ + { + "messageId": "pattern-message", + "messageParams": { + "pattern": "^[0-9]+$" + }, + "instanceLocation": "#", + "schemaLocations": ["#/oneOf/0/pattern"] + } + ], + [ + { + "messageId": "format-message", + "messageParams": { + "format": "email" + }, + "instanceLocation": "#", + "schemaLocations": ["#/oneOf/1/format"] + } + ] + ], + "instanceLocation": "#", + "schemaLocations": ["#/oneOf"] + } + ] + }, + { + "description": "none of the alternatives match instance type, hence no filtering of alternatives", + "schema": { + "oneOf": [ + { + "type": "string", + "maxLength": 2 + }, + { + "type": "number", + "minimum": 2 + } + ] + }, + "instance": true, + "errors": [ + { + "messageId": "oneOf-message", + "messageParams": { + "matchCount": 0 + }, + "alternatives": [ + [ + { + "messageId": "type-message", + "messageParams": { + "expectedTypes": { "or": ["string"] } + }, + "instanceLocation": "#", + "schemaLocations": ["#/oneOf/0/type"] + } + ], + [ + { + "messageId": "type-message", + "messageParams": { + "expectedTypes": { "or": ["number"] } + }, + "instanceLocation": "#", + "schemaLocations": ["#/oneOf/1/type"] + } + ] + ], + "instanceLocation": "#", + "schemaLocations": ["#/oneOf"] + } + ] + }, + { + "description": "oneOf with $ref alternatives filters by type", + "compatibility": "2019", + "schema": { + "$defs": { + "bar": { + "type": "string", + "maxLength": 2 + }, + "baz": { + "type": "number", + "minimum": 2 + } + }, + "oneOf": [ + { + "$ref": "#/$defs/bar" + }, + { + "$ref": "#/$defs/baz" + } + ] + }, + "instance": "foo", + "errors": [ + { + "messageId": "maxLength-message", + "messageParams": { + "maxLength": 2 + }, + "instanceLocation": "#", + "schemaLocations": [ + "#/$defs/bar/maxLength" + ] + } + ] } ] }