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
41 changes: 37 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,38 @@ console.log(errors);
// ]
```

With `@hyperjump/json-schema`, you can also get error messages directly from
validation. Importing this package adds a `JSE` output format to
`@hyperjump/json-schema`. It uses the full results of evaluation rather than
only what the standard output formats report. Use the `locale` option to choose
the language of the messages. The default is `en-US`, which is currently the
only locale available.

```TypeScript
import { registerSchema, validate } from "@hyperjump/json-schema/draft-2020-12";
import { JSE } from "@hyperjump/json-schema-errors";

const schemaUri = "https://example.com/schema/string";
registerSchema({
$schema: "https://json-schema.org/draft/2020-12/schema",

type: "string"
});

const output = await validate(schemaUri, 42, { outputFormat: JSE, locale: "en-US" });
console.log(output);
// {
// valid: false,
// errors: [
// {
// message: "Expected a string",
// instanceLocation: "#",
// schemaLocations: ["https://example.com/schema/string#/type"]
// }
// ]
// }
```

If using this package with the results from another validator, you still need to
register the schema. Here's an example using `@cfworker/json-schema`.

Expand Down Expand Up @@ -145,8 +177,10 @@ addErrorHandler(async (normalizedErrors, instance, localization) => {

Simple applicator keywords that just evaluate subschemas and don't make any
assertions of their own don't need an error handler, only a normalization
handler. For example, support for the `allOf` keyword could look like the
following.
handler. Whether a keyword is a simple applicator comes from the
`simpleApplicator` property of its `@hyperjump/json-schema` keyword definition.
The results of its subschemas are merged into the results of the parent schema.
For example, support for the `allOf` keyword could look like the following.

```TypeScript
import { setNormalizationHandler, evaluateSchema } from "@hyperjump/json-schema-errors";
Expand All @@ -156,8 +190,7 @@ const KEYWORD_URI = "https://json-schema.org/keyword/allOf";
setNormalizationHandler(KEYWORD_URI, {
evaluate(allOf, instance, context) {
return allOf.map((schemaLocation) => evaluateSchema(schemaLocation, instance, context));
},
simpleApplicator: true
}
});
```

Expand Down
9 changes: 4 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"dependencies": {
"@fluent/bundle": "^0.19.1",
"@hyperjump/json-pointer": "^1.1.1",
"@hyperjump/json-schema": "^1.17.2",
"@hyperjump/json-schema": "^1.18.0",
"@hyperjump/pact": "^1.4.0",
"@hyperjump/uri": "^1.3.2",
"json-stringify-deterministic": "^1.0.12"
Expand Down
23 changes: 3 additions & 20 deletions src/error-handlers/dependentSchemas.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ import {
describeConditional,
evaluateRequirements,
getCompiledKeywordValue,
getErrors,
getSuccesses,
mergeOutputs
getSuccesses
} from "../json-schema-errors.js";

/**
Expand All @@ -16,23 +14,8 @@ import {

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

for (const schemaLocation in normalizedErrors["https://json-schema.org/keyword/dependentSchemas"]) {
const dependentSchemas = normalizedErrors["https://json-schema.org/keyword/dependentSchemas"][schemaLocation];
if (dependentSchemas.valid !== false) {
continue;
}

// Merged so errors from different dependencies can be combined
const merged = mergeOutputs(dependentSchemas.outputs ?? []);
errors.push(...getErrors(merged, instance, localization, ast));
}

return errors;
},
// 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) => {
/** @type ErrorObject[] */
Expand Down
22 changes: 2 additions & 20 deletions src/error-handlers/ifThenElse.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {
describeConditional,
evaluateRequirements,
getCompiledKeywordValue,
getErrors,
getSuccesses,
someTrue
} from "../json-schema-errors.js";
Expand All @@ -15,25 +14,8 @@ import {

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

for (const keywordUri of ["https://json-schema.org/keyword/then", "https://json-schema.org/keyword/else"]) {
for (const schemaLocation in normalizedErrors[keywordUri]) {
const keywordOutput = normalizedErrors[keywordUri][schemaLocation];
if (keywordOutput.valid !== false) {
continue;
}

for (const subschemaOutput of keywordOutput.outputs ?? []) {
errors.push(...getErrors(subschemaOutput, instance, localization, ast));
}
}
}

return errors;
},
// Failures in 'then' and 'else' 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) => {
/** @type ErrorObject[] */
Expand Down
4 changes: 2 additions & 2 deletions src/hyperjump-json-schema.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import "@hyperjump/json-schema/draft-06";
import "@hyperjump/json-schema/draft-04";
import "@hyperjump/json-schema/formats";
import { BASIC } from "@hyperjump/json-schema/experimental";
import { jsonSchemaErrors, validate as validateWithErrors } from "../src/index.js";
import { JSE, jsonSchemaErrors } from "../src/index.js";
import { FluentBundle, FluentResource } from "@fluent/bundle";
import { translations } from "./translations/index.js";

Expand Down Expand Up @@ -88,7 +88,7 @@ const runTests = (dialectUri, dialect) => {
expect(errors).to.eql(buildErrors(testCase.errors, schemaUri));
expectNestedLocations(errors);

const result = await validateWithErrors(schemaUri, instance);
const result = await validate(schemaUri, instance, JSE);
const fullResultsErrors = result.valid ? [] : result.errors;
expect(fullResultsErrors).to.eql(buildErrors(testCase.errorsWithFullResults ?? testCase.errors, schemaUri));
expectNestedLocations(fullResultsErrors);
Expand Down
78 changes: 31 additions & 47 deletions src/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AST, CompiledSchema, EvaluationPlugin } from "@hyperjump/json-schema/experimental";
import { AST, EvaluationPlugin } from "@hyperjump/json-schema/experimental";
import { JsonNode } from "@hyperjump/json-schema/instance/experimental";
import { Localization } from "./localization.js";

Expand Down Expand Up @@ -89,25 +89,15 @@ export type NormalizationHandler<KeywordValue = unknown, Context extends Evaluat
evaluate(value: KeywordValue, instance: JsonNode, context: Context): NormalizedOutput[] | void;

/**
* Simple applicators just apply subschemas and don't have any validation behavior
* of their own. For example, `allOf` and `properties` are simple applicators. They
* never fail. Only their subschema can fail. `anyOf` and `oneOf` are not simple
* applicators because they can fail independently of the validation result of
* their subschemas.
*
* The results of a simple applicator's subschemas are merged into the results
* of its parent schema. Its own result is recorded too so it can be described,
* and like `validityFromSubschemas`, it fails if any of its subschemas fail.
*/
simpleApplicator?: true;

/**
* Some applicators, like `then` and `else`, only fail when their subschema
* fails. Validators often only report the subschema's errors and not the
* keyword itself, so this tells us to use the subschema's results when the
* validator's output doesn't include the keyword.
* Conditional applicators, like `then` and `dependentSchemas`, are simple
* applicators whose subschemas only apply when a condition holds. Whether a
* keyword is a simple applicator comes from its `@hyperjump/json-schema`
* keyword definition. The results of a simple applicator's subschemas are
* merged into the results of its parent schema, but the merged results of a
* conditional applicator are left out when describing the parent schema
* because its error handler describes them along with the condition.
*/
validityFromSubschemas?: true;
conditional?: true;

/**
* Annotations, such as `title` and `description`, never affect validation. A
Expand Down Expand Up @@ -224,39 +214,33 @@ export type ContainsRange = {
};

/**
* Validate an instance against a schema and get error messages in one step instead
* of getting output from validation and passing it to jsonSchemaErrors. The
* function is curried so you can compile the schema one time and evaluate multiple
* instances against the same compiled schema.
* An output format for `@hyperjump/json-schema` that returns human readable error
* messages. Importing this package registers it.
*
* Ideally, this function should be in @hyperjump/json-schema instead and this will
* be removed in the future.
*
* @deprecated
* @example
* const output = await validate(schemaUri, instance, { outputFormat: JSE, locale: "en-US" });
*/
export const validate: (
(schemaUri: string) => Promise<EvaluateInstance>
) & (
(schemaUri: string, instance: Json, options?: ValidationOptions) => Promise<ValidationResult>
);

export const evaluateCompiledSchema: (compiledSchema: CompiledSchema, instance: Json, options?: ValidationOptions) => ValidationResult;

export type EvaluateInstance = (instance: Json, options?: ValidationOptions) => ValidationResult;

export type ValidationOptions = {
/**
* A locale identifier in the form of "{language}-{region}".
*
* @example "en-US"
*/
locale?: string;
plugins?: EvaluationPlugin[];
};
export const JSE: "JSE";

export type ValidationResult = {
export type JSEOutput = {
valid: true;
} | {
valid: false;
errors: JsonSchemaErrors;
};

declare module "@hyperjump/json-schema" {
interface OutputFormats {
JSE: JSEOutput;
}

interface ValidationOptions<F extends OutputFormat = OutputFormat> {
/**
* A locale identifier in the form of "{language}-{region}" used for the
* messages of the JSE output format.
*
* @example "en-US"
*/
locale?: string;
}
}
5 changes: 2 additions & 3 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,6 @@ export {
jsonSchemaErrors,
removeErrorHandler,
setErrorHandler,
setNormalizationHandler,
validate,
evaluateCompiledSchema
setNormalizationHandler
} from "./json-schema-errors.js";
export { JSE } from "./output-format.js";
Loading
Loading