Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions src/error-handlers/anyOf.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as Instance from "@hyperjump/json-schema/instance/experimental";
import * as Pact from "@hyperjump/pact";
import { allTrue, flattenOutput, getErrors, getSuccesses, someTrue } from "../json-schema-errors.js";
import { allTrue, flattenOutput, getErrorsFromFlattenedOutput, getSuccesses, someTrue } from "../json-schema-errors.js";

/**
* @import { ErrorHandler, ErrorObject, InstanceOutput } from "../index.d.ts"
Expand Down Expand Up @@ -34,9 +34,7 @@ const anyOfErrorHandler = {
const alternatives = [];
const instanceLocation = Instance.uri(instance);

for (const [index, alternative] of anyOf.entries()) {
const results = alternativeResults[index];

for (const results of alternativeResults) {
// Filter alternatives whose declared type doesn't match the instance type
const typeResults = results[instanceLocation]?.["https://json-schema.org/keyword/type"];
if (typeResults && !Object.values(typeResults).every(({ valid }) => valid)) {
Expand All @@ -56,13 +54,13 @@ const anyOfErrorHandler = {
}

// The alternative passed all the filters
alternatives.push(getErrors(alternative, instance, context));
alternatives.push(getErrorsFromFlattenedOutput(results, instance, context));
}

// If all alternatives were filtered out, default to returning all of them
if (alternatives.length === 0) {
for (const alternative of anyOf) {
alternatives.push(getErrors(alternative, instance, context));
for (const results of alternativeResults) {
alternatives.push(getErrorsFromFlattenedOutput(results, instance, context));
}
}

Expand Down
17 changes: 8 additions & 9 deletions src/error-handlers/oneOf.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as Instance from "@hyperjump/json-schema/instance/experimental";
import * as Pact from "@hyperjump/pact";
import { allowsAnyValue, allTrue, countTrue, flattenOutput, getErrors, getSuccesses, getValidity, negate, someTrue } from "../json-schema-errors.js";
import { allowsAnyValue, allTrue, countTrue, flattenOutput, getErrorsFromFlattenedOutput, getSuccesses, getValidityFromFlattenedOutput, negate, someTrue } from "../json-schema-errors.js";

/**
* @import { JsonNode } from "@hyperjump/json-schema/instance/experimental"
Expand All @@ -19,10 +19,12 @@ const oneOfErrorHandler = {
continue;
}
const oneOf = oneOfOutput.outputs ?? [];
const alternativeResults = oneOf.map(flattenOutput);

const alternativeLocations = /** @type string[] */ (normalizedErrors["https://json-schema.org/keyword/oneOf"][schemaLocation].value);
// Descriptions need the original output to keep conditional requirements separate.
const matches = alternativeLocations.flatMap((alternativeLocation, index) => {
return getValidity(oneOf[index]) === true ? [{ alternativeLocation, output: oneOf[index] }] : [];
return getValidityFromFlattenedOutput(alternativeResults[index]) === true ? [{ alternativeLocation, output: oneOf[index] }] : [];
});
if (matches.length > 1) {
errors.push(multipleMatchesError(matches, schemaLocation, instance, context));
Expand All @@ -35,17 +37,14 @@ const oneOfErrorHandler = {
Pact.collectArray
);

const alternativeResults = oneOf.map(flattenOutput);
const discriminators = propertyLocations.filter((propertyLocation) => {
return alternativeResults.some((results) => isPassingProperty(results[propertyLocation]));
});

const alternatives = [];
const instanceLocation = Instance.uri(instance);

for (const [index, alternative] of oneOf.entries()) {
const results = alternativeResults[index];

for (const results of alternativeResults) {
// Filter alternatives whose declared type doesn't match the instance type
const typeResults = results[instanceLocation]?.["https://json-schema.org/keyword/type"];
if (typeResults && !Object.values(typeResults).every(({ valid }) => valid)) {
Expand All @@ -65,16 +64,16 @@ const oneOfErrorHandler = {
}

// The alternative passed all the filters
const alternativeErrors = getErrors(alternative, instance, context);
const alternativeErrors = getErrorsFromFlattenedOutput(results, instance, context);
if (alternativeErrors.length) {
alternatives.push(alternativeErrors);
}
}

// If all alternatives were filtered out, default to returning all of them
if (alternatives.length === 0) {
for (const alternative of oneOf) {
const alternativeErrors = getErrors(alternative, instance, context);
for (const results of alternativeResults) {
const alternativeErrors = getErrorsFromFlattenedOutput(results, instance, context);
if (alternativeErrors.length) {
alternatives.push(alternativeErrors);
}
Expand Down
12 changes: 10 additions & 2 deletions src/json-schema-errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -443,10 +443,14 @@ export const removeErrorHandler = (errorHandlerUri) => {

/** @type API.getErrors */
export const getErrors = (normalizedErrors, rootInstance, context) => {
return getErrorsFromFlattenedOutput(flattenOutput(normalizedErrors), rootInstance, context);
};

/** @type API.getErrors */
export const getErrorsFromFlattenedOutput = (reportedOutput, rootInstance, context) => {
/** @type API.ErrorObject[] */
const errors = [];

const reportedOutput = flattenOutput(normalizedErrors);
for (const instanceLocation in reportedOutput) {
const instance = /** @type JsonNode */ (Instance.get(instanceLocation, rootInstance));
for (const errorHandlerUri in errorHandlers) {
Expand Down Expand Up @@ -679,10 +683,14 @@ const sizeOf = (errorObjects) => errorObjects.reduce((size, errorObject) => {
* @type API.getValidity
*/
export const getValidity = (normalizedOutput) => {
return getValidityFromFlattenedOutput(flattenOutput(normalizedOutput));
};

/** @type API.getValidity */
export const getValidityFromFlattenedOutput = (results) => {
/** @type boolean | undefined */
let validity = true;

const results = flattenOutput(normalizedOutput);
for (const instanceLocation in results) {
for (const keywordUri in results[instanceLocation]) {
for (const schemaLocation in results[instanceLocation][keywordUri]) {
Expand Down
32 changes: 32 additions & 0 deletions src/test-suite/tests/anyOf.json
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,38 @@
"schemaLocations": ["#/anyOf/1/properties/b/type"]
}
]
},
{
"description": "anyOf reports an applied conditional in a selected alternative",
"compatibility": "7",
"schema": {
"anyOf": [
{
"if": {
"type": "number"
},
"then": {
"minimum": 5
}
},
{
"type": "string"
}
]
},
"instance": 1,
"errors": [
{
"messageId": "minimum-message",
"messageParams": {
"minimum": 5
},
"instanceLocation": "#",
"schemaLocations": [
"#/anyOf/0/then/minimum"
]
}
]
}
]
}
172 changes: 172 additions & 0 deletions src/test-suite/tests/oneOf.json
Original file line number Diff line number Diff line change
Expand Up @@ -2324,6 +2324,178 @@
"schemaLocations": ["#/oneOf"]
}
]
},
{
"description": "oneOf reports an applied conditional in a selected alternative",
"compatibility": "7",
"schema": {
"oneOf": [
{
"if": {
"type": "number"
},
"then": {
"minimum": 5
}
},
{
"type": "string"
}
]
},
"instance": 1,
"errors": [
{
"messageId": "minimum-message",
"messageParams": {
"minimum": 5
},
"instanceLocation": "#",
"schemaLocations": [
"#/oneOf/0/then/minimum"
]
}
]
},
{
"description": "oneOf matching alternative keeps direct and conditional references separate",
"compatibility": "2019",
"schema": {
"oneOf": [
{
"$ref": "#/$defs/user"
},
{
"required": [
"other"
]
}
],
"$defs": {
"base": {
"required": [
"id"
]
},
"admin": {
"$ref": "#/$defs/base",
"required": [
"permissions"
]
},
"user": {
"$ref": "#/$defs/base",
"if": {
"required": [
"admin"
]
},
"then": {
"$ref": "#/$defs/admin"
}
}
}
},
"instance": {
"id": 1,
"admin": true,
"permissions": [],
"other": true
},
"errors": [
{
"messageId": "oneOf-multiple-matches-message",
"alternatives": [
[
{
"messageId": "count-true-message",
"messageParams": {
"kind": "atLeast",
"min": 1
},
"alternatives": [
[
{
"messageId": "required-negated-message",
"messageParams": {
"count": 1,
"required": {
"or": [
"'admin'"
]
}
},
"instanceLocation": "#",
"schemaLocations": [
"#/$defs/user/if/required"
]
}
],
[
{
"messageId": "required-success-message",
"messageParams": {
"count": 2,
"required": {
"and": [
"'id'",
"'permissions'"
]
}
},
"instanceLocation": "#",
"schemaLocations": [
"#/$defs/base/required",
"#/$defs/admin/required"
]
}
]
],
"instanceLocation": "#",
"schemaLocations": [
"#/$defs/user/if"
]
},
{
"messageId": "required-success-message",
"messageParams": {
"count": 1,
"required": {
"and": [
"'id'"
]
}
},
"instanceLocation": "#",
"schemaLocations": [
"#/$defs/base/required"
]
}
],
[
{
"messageId": "required-success-message",
"messageParams": {
"count": 1,
"required": {
"and": [
"'other'"
]
}
},
"instanceLocation": "#",
"schemaLocations": [
"#/oneOf/1/required"
]
}
]
],
"instanceLocation": "#",
"schemaLocations": [
"#/oneOf"
]
}
]
}
]
}
Loading