Skip to content
Merged
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
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,16 @@ console.log(errors);
// ]
```

Validators often only validate the `format` keyword if they're configured to.
Use the `isFormatAsserted` option to say whether the validator validated
formats. If it isn't given, messages that describe `format` say that it only
applies if formats are validated. The `JSE` output format works this out from
`@hyperjump/json-schema`'s configuration and each dialect's default.

```TypeScript
const errors = await jsonSchemaErrors(output, schemaUri, instance, { isFormatAsserted: true });
```

## API

https://json-schema-errors.hyperjump.io
Expand Down
12 changes: 6 additions & 6 deletions src/error-handlers/anyOf.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { allTrue, getErrors, getSuccesses, someTrue } from "../json-schema-error

/** @type ErrorHandler */
const anyOfErrorHandler = {
error: (normalizedErrors, instance, localization, ast) => {
error: (normalizedErrors, instance, localization, context) => {
/** @type ErrorObject[] */
const errors = [];

Expand Down Expand Up @@ -53,13 +53,13 @@ const anyOfErrorHandler = {
}

// The alternative passed all the filters
alternatives.push(getErrors(alternative, instance, localization, ast));
alternatives.push(getErrors(alternative, instance, localization, 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, localization, ast));
alternatives.push(getErrors(alternative, instance, localization, context));
}
}

Expand All @@ -78,7 +78,7 @@ const anyOfErrorHandler = {
return errors;
},

success: (normalizedOutput, instance, localization, ast) => {
success: (normalizedOutput, instance, localization, context) => {
/** @type ErrorObject[] */
const successes = [];

Expand All @@ -91,7 +91,7 @@ const anyOfErrorHandler = {
// alternative fail could make another one pass, so all of them need to
// be described even if some of them fail now.
const alternativeOptions = alternatives.map((alternative) => {
return getSuccesses(alternative, instance, localization, ast);
return getSuccesses(alternative, instance, localization, context);
});

// An alternative that can't be described means we can't say how to make it fail
Expand All @@ -112,7 +112,7 @@ const anyOfErrorHandler = {
// even if we know which ones match, because these descriptions tell the
// user what would need to change to make 'anyOf' fail.
const descriptions = alternatives.map((alternative) => {
return getSuccesses(alternative, instance, localization, ast);
return getSuccesses(alternative, instance, localization, context);
});

if (descriptions.some((description) => description.length === 0)) {
Expand Down
20 changes: 10 additions & 10 deletions src/error-handlers/contains.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const keywordUris = [

/** @type ErrorHandler */
const containsErrorHandler = {
error: (normalizedErrors, instance, localization, ast) => {
error: (normalizedErrors, instance, localization, context) => {
/** @type ErrorObject[] */
const errors = [];

Expand All @@ -27,20 +27,20 @@ const containsErrorHandler = {
/** @type string[] */
const schemaLocations = [schemaLocation];

const contains = /** @type ContainsAst */ (getCompiledKeywordValue(ast, schemaLocation));
const contains = /** @type ContainsAst */ (getCompiledKeywordValue(context.ast, schemaLocation));

/** @type ContainsRange */
const range = {};
if (typeof contains !== "string") {
if (contains.minContains !== 1) {
range.minContains = contains.minContains;
const minContainsLocation = getSiblingKeywordLocation(ast, schemaLocation, "https://json-schema.org/keyword/minContains");
const minContainsLocation = getSiblingKeywordLocation(context.ast, schemaLocation, "https://json-schema.org/keyword/minContains");
schemaLocations.push(minContainsLocation);
}

if (contains.maxContains !== Number.MAX_SAFE_INTEGER) {
range.maxContains = contains.maxContains;
const maxContainsLocation = getSiblingKeywordLocation(ast, schemaLocation, "https://json-schema.org/keyword/maxContains");
const maxContainsLocation = getSiblingKeywordLocation(context.ast, schemaLocation, "https://json-schema.org/keyword/maxContains");
schemaLocations.push(maxContainsLocation);

// Too many items matched. Report on each matching item how it
Expand All @@ -53,7 +53,7 @@ const containsErrorHandler = {
});
if (matches.length > contains.maxContains) {
const descriptions = matches.map(({ itemOutput }) => {
return getSuccesses(itemOutput, instance, localization, ast);
return getSuccesses(itemOutput, instance, localization, context);
});

// If any match can't be described, the errors would be misleading
Expand Down Expand Up @@ -84,7 +84,7 @@ const containsErrorHandler = {
// Describe what an item would need to be like using an item that doesn't exist
const containsLocation = typeof contains === "string" ? contains : contains.contains;
const item = getPlaceholder(instance, String(Instance.length(instance)));
const description = describeEach(containsLocation, item, instance, localization, ast);
const description = describeEach(containsLocation, item, instance, localization, context);

if (description.length > 0) {
errors.push({
Expand All @@ -93,7 +93,7 @@ const containsErrorHandler = {
instanceLocation: Instance.uri(instance),
schemaLocations: schemaLocations
});
} else if (allowsAnyValue(containsLocation, ast)) {
} else if (allowsAnyValue(containsLocation, context.ast)) {
// Any item matches, so there aren't enough items
errors.push({
message: localization.getMinItemsErrorMessage(range.minContains ?? 1),
Expand All @@ -113,15 +113,15 @@ const containsErrorHandler = {
return errors;
},

success: (normalizedOutput, instance, localization, ast) => {
success: (normalizedOutput, instance, localization, context) => {
/** @type ErrorObject[] */
const successes = [];

for (const keywordUri of keywordUris) {
for (const schemaLocation in normalizedOutput[keywordUri]) {
const itemOutputs = normalizedOutput[keywordUri][schemaLocation].outputs ?? [];

const contains = /** @type ContainsAst | string */ (getCompiledKeywordValue(ast, schemaLocation));
const contains = /** @type ContainsAst | string */ (getCompiledKeywordValue(context.ast, schemaLocation));
const minContains = typeof contains === "string" ? 1 : contains.minContains;
const maxContains = typeof contains === "string" || contains.maxContains === Number.MAX_SAFE_INTEGER
? Infinity
Expand All @@ -132,7 +132,7 @@ const containsErrorHandler = {
const notMatching = itemOutputs.filter((itemOutput) => !isPassing(itemOutput));

/** @type (itemOutput: NormalizedOutput, localization: Localization) => ErrorObject[] */
const describe = (itemOutput, localization) => getSuccesses(itemOutput, instance, localization, ast);
const describe = (itemOutput, localization) => getSuccesses(itemOutput, instance, localization, context);

if (localization.isNegated) {
// 'contains' fails if too few items match or too many items match
Expand Down
17 changes: 8 additions & 9 deletions src/error-handlers/dependentSchemas.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,23 @@ import {
} from "../json-schema-errors.js";

/**
* @import { AST } from "@hyperjump/json-schema/experimental"
* @import { JsonNode } from "@hyperjump/json-schema/instance/experimental"
* @import { ErrorHandler, ErrorObject, Localization, NormalizedOutput } from "../index.d.ts"
* @import { ErrorHandler, ErrorHandlerContext, ErrorObject, Localization, NormalizedOutput } from "../index.d.ts"
*/

/** @type ErrorHandler */
const dependentSchemasErrorHandler = {
// Failures in dependent schemas are merged into the parent schema's results,
// so they're reported by the handlers for the keywords that failed

success: (normalizedOutput, instance, localization, ast) => {
success: (normalizedOutput, instance, localization, context) => {
/** @type ErrorObject[] */
const successes = [];

for (const schemaLocation in normalizedOutput["https://json-schema.org/keyword/dependentSchemas"]) {
const dependencies = /** @type [string, string][] */ (getCompiledKeywordValue(ast, schemaLocation));
const dependencies = /** @type [string, string][] */ (getCompiledKeywordValue(context.ast, schemaLocation));
const outputs = normalizedOutput["https://json-schema.org/keyword/dependentSchemas"][schemaLocation].outputs ?? [];
successes.push(...describeSchemaDependencies(dependencies, outputs, instance, localization, ast));
successes.push(...describeSchemaDependencies(dependencies, outputs, instance, localization, context));
}

return successes;
Expand All @@ -36,9 +35,9 @@ const dependentSchemasErrorHandler = {
* dependency's property. The outputs are for the dependencies whose property is
* present, in the order they appear in the schema.
*
* @type (dependencies: [string, string][], outputs: NormalizedOutput[], instance: JsonNode, localization: Localization, ast: AST) => ErrorObject[]
* @type (dependencies: [string, string][], outputs: NormalizedOutput[], instance: JsonNode, localization: Localization, context: ErrorHandlerContext) => ErrorObject[]
*/
export const describeSchemaDependencies = (dependencies, outputs, instance, localization, ast) => {
export const describeSchemaDependencies = (dependencies, outputs, instance, localization, context) => {
/** @type ErrorObject[] */
const successes = [];

Expand All @@ -48,7 +47,7 @@ export const describeSchemaDependencies = (dependencies, outputs, instance, loca
for (const [propertyName, dependencyLocation] of dependencies) {
const isPresent = isObject && Instance.has(propertyName, instance);
const output = (isPresent ? outputs[outputIndex++] : undefined)
?? evaluateRequirements(dependencyLocation, instance, ast);
?? evaluateRequirements(dependencyLocation, instance, context.ast);

// The dependency only applies if the property is present
successes.push(...describeConditional({
Expand All @@ -57,7 +56,7 @@ export const describeSchemaDependencies = (dependencies, outputs, instance, loca
instanceLocation: Instance.uri(instance),
schemaLocations: [dependencyLocation]
}],
then: (localization) => getSuccesses(output, instance, localization, ast)
then: (localization) => getSuccesses(output, instance, localization, context)
}, instance, dependencyLocation, localization));
}

Expand Down
10 changes: 5 additions & 5 deletions src/error-handlers/draft-04/dependencies.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { describeSchemaDependencies } from "../dependentSchemas.js";

/** @type ErrorHandler */
const dependenciesErrorHandler = {
error: (normalizedErrors, instance, localization, ast) => {
error: (normalizedErrors, instance, localization, context) => {
/** @type ErrorObject[] */
const errors = [];

Expand All @@ -18,24 +18,24 @@ const dependenciesErrorHandler = {

const dependentSchemaOutputs = normalizedErrors["https://json-schema.org/keyword/draft-04/dependencies"][schemaLocation].outputs ?? [];
for (const dependentSchemaOutput of dependentSchemaOutputs) {
const dependentSchemaErrors = getErrors(dependentSchemaOutput, instance, localization, ast);
const dependentSchemaErrors = getErrors(dependentSchemaOutput, instance, localization, context);
errors.push(...dependentSchemaErrors);
}
}

return errors;
},

success: (normalizedOutput, instance, localization, ast) => {
success: (normalizedOutput, instance, localization, context) => {
/** @type ErrorObject[] */
const successes = [];

for (const schemaLocation in normalizedOutput["https://json-schema.org/keyword/draft-04/dependencies"]) {
// Array-form dependencies are handled with 'required'
const dependencies = /** @type [string, string | string[]][] */ (getCompiledKeywordValue(ast, schemaLocation));
const dependencies = /** @type [string, string | string[]][] */ (getCompiledKeywordValue(context.ast, schemaLocation));
const schemaDependencies = /** @type [string, string][] */ (dependencies.filter(([, dependency]) => typeof dependency === "string"));
const outputs = normalizedOutput["https://json-schema.org/keyword/draft-04/dependencies"][schemaLocation].outputs ?? [];
successes.push(...describeSchemaDependencies(schemaDependencies, outputs, instance, localization, ast));
successes.push(...describeSchemaDependencies(schemaDependencies, outputs, instance, localization, context));
}

return successes;
Expand Down
18 changes: 9 additions & 9 deletions src/error-handlers/eachProperty.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
* @type ErrorHandler
*/
const eachPropertyErrorHandler = {
success: (normalizedOutput, instance, localization, ast) => {
success: (normalizedOutput, instance, localization, context) => {
/** @type ErrorObject[] */
const successes = [];

Expand All @@ -37,43 +37,43 @@ const eachPropertyErrorHandler = {
const property = getPlaceholder(instance, unusedPropertyName(instance));

for (const schemaLocation in normalizedOutput["https://json-schema.org/keyword/patternProperties"]) {
const patternProperties = /** @type [RegExp, string][] */ (getCompiledKeywordValue(ast, schemaLocation));
const patternProperties = /** @type [RegExp, string][] */ (getCompiledKeywordValue(context.ast, schemaLocation));
for (const [pattern, subschemaLocation] of patternProperties) {
successes.push(...describeScope({
subschemaLocation,
placeholder: property,
each: (localization, count) => localization.getEachMatchingPropertySuccessMessage(pattern.source, count),
none: (localization) => localization.getNoMatchingPropertySuccessMessage(pattern.source)
}, instance, schemaLocation, localization, ast));
}, instance, schemaLocation, localization, context));
}
}

for (const schemaLocation in normalizedOutput["https://json-schema.org/keyword/additionalProperties"]) {
const [, subschemaLocation] = /** @type [RegExp, string] */ (getCompiledKeywordValue(ast, schemaLocation));
const [, subschemaLocation] = /** @type [RegExp, string] */ (getCompiledKeywordValue(context.ast, schemaLocation));
const properties = Object.keys(/** @type Record<string, string> */ (
getSiblingValue(ast, schemaLocation, "https://json-schema.org/keyword/properties") ?? {}
getSiblingValue(context.ast, schemaLocation, "https://json-schema.org/keyword/properties") ?? {}
));
const patterns = /** @type [RegExp, string][] */ (
getSiblingValue(ast, schemaLocation, "https://json-schema.org/keyword/patternProperties") ?? []
getSiblingValue(context.ast, schemaLocation, "https://json-schema.org/keyword/patternProperties") ?? []
).map(([pattern]) => pattern.source);
successes.push(...describeScope({
subschemaLocation,
placeholder: property,
each: (localization, count) => localization.getEachAdditionalPropertySuccessMessage(properties, patterns, count),
none: (localization) => localization.getNoAdditionalPropertySuccessMessage(properties, patterns)
}, instance, schemaLocation, localization, ast));
}, instance, schemaLocation, localization, context));
}

// Property names are at a different location than property values
const propertyName = getPropertyNamePlaceholder(instance, unusedPropertyName(instance));
for (const schemaLocation in normalizedOutput["https://json-schema.org/keyword/propertyNames"]) {
const subschemaLocation = /** @type string */ (getCompiledKeywordValue(ast, schemaLocation));
const subschemaLocation = /** @type string */ (getCompiledKeywordValue(context.ast, schemaLocation));
successes.push(...describeScope({
subschemaLocation,
placeholder: propertyName,
each: (localization, count) => localization.getEachPropertyNameSuccessMessage(count),
none: (localization) => localization.getMaxPropertiesSuccessMessage(0)
}, instance, schemaLocation, localization, ast));
}, instance, schemaLocation, localization, context));
}

return successes;
Expand Down
24 changes: 15 additions & 9 deletions src/error-handlers/format.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { describeKeyword, getCompiledKeywordValue } from "../json-schema-errors.
*/

const keywordUris = [
"https://json-schema.org/keyword/format",
"https://json-schema.org/keyword/draft-2020-12/format",
"https://json-schema.org/keyword/draft-2020-12/format-assertion",
"https://json-schema.org/keyword/draft-2019-09/format",
Expand All @@ -17,7 +18,7 @@ const keywordUris = [

/** @type ErrorHandler */
const formatErrorHandler = {
error: (normalizedErrors, instance, localization, ast) => {
error: (normalizedErrors, instance, localization, context) => {
/** @type ErrorObject[] */
const errors = [];

Expand All @@ -27,7 +28,7 @@ const formatErrorHandler = {
continue;
}

const format = /** @type string */ (getCompiledKeywordValue(ast, schemaLocation));
const format = /** @type string */ (getCompiledKeywordValue(context.ast, schemaLocation));

errors.push({
message: localization.getFormatErrorMessage(format),
Expand All @@ -40,14 +41,19 @@ const formatErrorHandler = {
return errors;
},

success: (normalizedOutput, instance, localization, ast) => {
// Whether 'format' is an assertion depends on how the validator is
// configured, which we don't know, so the messages say that it only applies
// if formats are validated. That includes 'format-assertion' because some
// validators can be configured not to validate it either.
success: (normalizedOutput, instance, localization, context) => {
// A 'format' that's only an annotation doesn't require anything. If it's
// not known, the message says that it only applies if formats are validated.
return keywordUris.flatMap((keywordUri) => {
return describeKeyword(normalizedOutput, keywordUri, instance, ast, (/** @type string */ format) => {
return localization.getFormatSuccessMessage(format);
return describeKeyword(normalizedOutput, keywordUri, instance, context.ast, (/** @type string */ format) => {
switch (context.isFormatAsserted(keywordUri, format)) {
case true:
return localization.getFormatSuccessMessage(format);
case false:
return undefined;
default:
return localization.getFormatIfValidatedSuccessMessage(format);
}
});
});
}
Expand Down
Loading
Loading