From a91f8690d7d9ec672f74d58470765c160fe2e259 Mon Sep 17 00:00:00 2001 From: Jason Desrosiers Date: Sun, 4 Oct 2026 21:43:01 -0700 Subject: [PATCH] Move validate to a JSE output format for @hyperjump/json-schema Importing this package now registers a JSE output format with @hyperjump/json-schema's setOutputFormat, so validation can return error messages directly. This replaces validate and evaluateCompiledSchema. The output plugin stays in this package. JSE's output type and the locale option are added to @hyperjump/json-schema's types by augmenting OutputFormats and ValidationOptions. Whether a keyword is a simple applicator now comes from its @hyperjump/json-schema keyword definition instead of being duplicated on normalization handlers. 'then', 'else', and 'dependentSchemas' are simple applicators there, so their results are merged into their parent schema. Their normalization handlers are marked as conditional so their merged results aren't described as requirements of the parent, since their handlers describe them along with the condition. Applicators keep their subschemas' outputs so they can be described. This replaces validityFromSubschemas. This requires @hyperjump/json-schema 1.18.0, which adds setOutputFormat and no longer treats 'if' as a simple applicator. Co-Authored-By: Claude Opus 5.5 --- README.md | 41 +- package-lock.json | 9 +- package.json | 2 +- src/error-handlers/dependentSchemas.js | 23 +- src/error-handlers/ifThenElse.js | 22 +- src/hyperjump-json-schema.test.js | 4 +- src/index.d.ts | 78 ++- src/index.js | 5 +- src/json-schema-errors.js | 142 +++-- .../additionalProperties.js | 3 +- src/normalization-handlers/allOf.js | 3 +- .../dependentSchemas.js | 2 +- .../draft-04/additionalItems.js | 3 +- src/normalization-handlers/draft-04/items.js | 3 +- src/normalization-handlers/dynamicRef.js | 3 +- src/normalization-handlers/else.js | 2 +- src/normalization-handlers/items.js | 3 +- .../patternProperties.js | 3 +- src/normalization-handlers/prefixItems.js | 3 +- src/normalization-handlers/properties.js | 3 +- src/normalization-handlers/propertyNames.js | 3 +- src/normalization-handlers/ref.js | 3 +- src/normalization-handlers/then.js | 2 +- .../unevaluatedItems.js | 3 +- .../unevaluatedProperties.js | 3 +- src/output-format.js | 24 + src/output-plugin.js | 15 +- src/output-plugin.test.ts | 504 ++++++++++++++++-- 28 files changed, 670 insertions(+), 244 deletions(-) create mode 100644 src/output-format.js diff --git a/README.md b/README.md index 6d803ea..4c9b340 100644 --- a/README.md +++ b/README.md @@ -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`. @@ -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"; @@ -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 + } }); ``` diff --git a/package-lock.json b/package-lock.json index 13978e3..b91a783 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,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" @@ -356,9 +356,9 @@ } }, "node_modules/@hyperjump/json-schema": { - "version": "1.17.8", - "resolved": "https://registry.npmjs.org/@hyperjump/json-schema/-/json-schema-1.17.8.tgz", - "integrity": "sha512-XOqbR9GRNHaH4JEXHdbsm7xfYwudZG7HVDq3qPZUb1gi+ZQPklgNvhMi6zf0Plf433qR61MK+xeeprUwUUvGPg==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@hyperjump/json-schema/-/json-schema-1.18.0.tgz", + "integrity": "sha512-YPLJsCosW6pHhulKrFogUsl1WxQVRy+fY0pROt66FDFygjIWZkMxXyiy8HLRR1JKeAd5OE6NSZs0NVjSBNwb2Q==", "license": "MIT", "dependencies": { "@hyperjump/json-pointer": "^1.1.0", @@ -366,7 +366,6 @@ "@hyperjump/pact": "^1.2.0", "@hyperjump/uri": "^1.2.0", "content-type": "^1.0.4", - "json-stringify-deterministic": "^1.0.12", "just-curry-it": "^5.3.0", "uuid": "^14.0.0" }, diff --git a/package.json b/package.json index 350c1ef..58fd621 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/error-handlers/dependentSchemas.js b/src/error-handlers/dependentSchemas.js index 6e069ed..8ad9a09 100644 --- a/src/error-handlers/dependentSchemas.js +++ b/src/error-handlers/dependentSchemas.js @@ -3,9 +3,7 @@ import { describeConditional, evaluateRequirements, getCompiledKeywordValue, - getErrors, - getSuccesses, - mergeOutputs + getSuccesses } from "../json-schema-errors.js"; /** @@ -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[] */ diff --git a/src/error-handlers/ifThenElse.js b/src/error-handlers/ifThenElse.js index 90930bd..896c610 100644 --- a/src/error-handlers/ifThenElse.js +++ b/src/error-handlers/ifThenElse.js @@ -2,7 +2,6 @@ import { describeConditional, evaluateRequirements, getCompiledKeywordValue, - getErrors, getSuccesses, someTrue } from "../json-schema-errors.js"; @@ -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[] */ diff --git a/src/hyperjump-json-schema.test.js b/src/hyperjump-json-schema.test.js index f3d0f22..8749e74 100644 --- a/src/hyperjump-json-schema.test.js +++ b/src/hyperjump-json-schema.test.js @@ -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"; @@ -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); diff --git a/src/index.d.ts b/src/index.d.ts index f62cf6e..e1ed76e 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -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"; @@ -89,25 +89,15 @@ export type NormalizationHandler Promise -) & ( - (schemaUri: string, instance: Json, options?: ValidationOptions) => Promise -); - -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 { + /** + * A locale identifier in the form of "{language}-{region}" used for the + * messages of the JSE output format. + * + * @example "en-US" + */ + locale?: string; + } +} diff --git a/src/index.js b/src/index.js index 69e5573..cb497e5 100644 --- a/src/index.js +++ b/src/index.js @@ -191,7 +191,6 @@ export { jsonSchemaErrors, removeErrorHandler, setErrorHandler, - setNormalizationHandler, - validate, - evaluateCompiledSchema + setNormalizationHandler } from "./json-schema-errors.js"; +export { JSE } from "./output-format.js"; diff --git a/src/json-schema-errors.js b/src/json-schema-errors.js index f9376b8..94a7d63 100644 --- a/src/json-schema-errors.js +++ b/src/json-schema-errors.js @@ -1,10 +1,9 @@ -import { compile, getKeyword, getSchema, Validation } from "@hyperjump/json-schema/experimental"; +import { compile, getKeyword, getSchema } from "@hyperjump/json-schema/experimental"; import * as Instance from "@hyperjump/json-schema/instance/experimental"; import * as Schema from "@hyperjump/browser"; import * as JsonPointer from "@hyperjump/json-pointer"; import { toAbsoluteIri } from "@hyperjump/uri"; import { Localization } from "./localization.js"; -import { JsonSchemaErrorsOutputPlugin } from "./output-plugin.js"; /** * @import * as API from "./index.d.ts" @@ -41,11 +40,6 @@ export const allowsAnyValue = (schemaLocation, ast) => { return schemaNode.every(([keywordUri]) => normalizationHandlers[toAbsoluteIri(keywordUri)]?.annotation); }; -/** @type (keywordUri: string) => boolean */ -export const isSimpleApplicator = (keywordUri) => { - return normalizationHandlers[toAbsoluteIri(keywordUri)]?.simpleApplicator ?? false; -}; - /** @type API.setNormalizationHandler */ export const setNormalizationHandler = (schemaUri, handler) => { normalizationHandlers[schemaUri] = handler; @@ -155,7 +149,7 @@ export const evaluateSchema = (schemaLocation, instance, context) => { plugins: context.plugins, // The output won't say what happened inside an applicator that passed isValidityUnknown: context.isValidityUnknown || ( - !keyword.simpleApplicator && !keyword.validityFromSubschemas && isKeywordValid === true + !validationKeyword.simpleApplicator && isKeywordValid === true ) }; for (const plugin of context.plugins) { @@ -165,8 +159,7 @@ export const evaluateSchema = (schemaLocation, instance, context) => { const keywordOutput = keyword.evaluate(keywordValue, instance, keywordContext); const isReported = context.errorIndex[keywordLocation]?.[keywordInstanceLocation] !== undefined; - const isValidityFromSubschemas = keyword.validityFromSubschemas || keyword.simpleApplicator; - if (isValidityFromSubschemas && !isReported && keywordOutput?.some(isFailing)) { + if (validationKeyword.simpleApplicator && !isReported && keywordOutput?.some(isFailing)) { isKeywordValid = false; } @@ -174,15 +167,15 @@ export const evaluateSchema = (schemaLocation, instance, context) => { valid = false; } - if (keyword.simpleApplicator) { - for (const suboutput of /** @type API.NormalizedOutput[] */ (keywordOutput)) { + if (validationKeyword.simpleApplicator) { + for (const suboutput of keywordOutput ?? []) { mergeOutput(output, suboutput); } } output[instanceLocation] ??= {}; output[instanceLocation][normalizedKeywordUri] ??= {}; - output[instanceLocation][normalizedKeywordUri][keywordLocation] = !keyword.simpleApplicator && keywordOutput + output[instanceLocation][normalizedKeywordUri][keywordLocation] = keywordOutput ? { valid: isKeywordValid, outputs: keywordOutput } : { valid: isKeywordValid }; @@ -409,16 +402,6 @@ const relocate = (errorObject, from, to) => { return relocated; }; -/** @type (outputs: API.NormalizedOutput[]) => API.NormalizedOutput */ -export const mergeOutputs = (outputs) => { - /** @type API.NormalizedOutput */ - const merged = {}; - for (const output of outputs) { - mergeOutput(merged, output); - } - return merged; -}; - /** @type (a: API.NormalizedOutput, b: API.NormalizedOutput) => void */ const mergeOutput = (a, b) => { for (const instanceLocation in b) { @@ -460,6 +443,63 @@ export const getErrors = (normalizedErrors, rootInstance, localization, ast) => return errors; }; +/** + * The results of a conditional keyword's subschemas are merged into the results + * of its parent schema like any simple applicator, but they only apply when the + * condition holds. The conditional keyword's handler describes them along with + * the condition, so they aren't described again as requirements of the parent. + * + * @type (normalizedOutput: API.NormalizedOutput) => API.NormalizedOutput + */ +const withoutConditionalResults = (normalizedOutput) => { + /** @type Set */ + const conditionalResults = new Set(); + for (const instanceLocation in normalizedOutput) { + for (const keywordUri in normalizedOutput[instanceLocation]) { + if (!normalizationHandlers[toAbsoluteIri(keywordUri)]?.conditional) { + continue; + } + + for (const keywordLocation in normalizedOutput[instanceLocation][keywordUri]) { + for (const output of normalizedOutput[instanceLocation][keywordUri][keywordLocation].outputs ?? []) { + for (const subInstanceLocation in output) { + for (const subKeywordUri in output[subInstanceLocation]) { + for (const subKeywordLocation in output[subInstanceLocation][subKeywordUri]) { + conditionalResults.add(resultKey(subInstanceLocation, subKeywordUri, subKeywordLocation)); + } + } + } + } + } + } + } + + if (conditionalResults.size === 0) { + return normalizedOutput; + } + + /** @type API.NormalizedOutput */ + const result = {}; + for (const instanceLocation in normalizedOutput) { + for (const keywordUri in normalizedOutput[instanceLocation]) { + for (const keywordLocation in normalizedOutput[instanceLocation][keywordUri]) { + if (conditionalResults.has(resultKey(instanceLocation, keywordUri, keywordLocation))) { + continue; + } + + result[instanceLocation] ??= {}; + result[instanceLocation][keywordUri] ??= {}; + result[instanceLocation][keywordUri][keywordLocation] = normalizedOutput[instanceLocation][keywordUri][keywordLocation]; + } + } + } + + return result; +}; + +/** @type (instanceLocation: string, keywordUri: string, keywordLocation: string) => string */ +const resultKey = (instanceLocation, keywordUri, keywordLocation) => JSON.stringify([instanceLocation, keywordUri, keywordLocation]); + /** @type API.getSuccesses */ export const getSuccesses = (normalizedOutput, rootInstance, localization, ast) => { // Descriptions of nested subschemas can get very large, so stop describing @@ -478,14 +518,16 @@ export const getSuccesses = (normalizedOutput, rootInstance, localization, ast) /** @type API.ErrorObject[] */ const successes = []; + const describedOutput = withoutConditionalResults(normalizedOutput); + descriptionDepth++; try { - for (const instanceLocation in normalizedOutput) { + for (const instanceLocation in describedOutput) { // Descriptions can be about values that don't exist yet const instance = getInstance(instanceLocation, rootInstance) ?? toPlaceholder(instanceLocation, rootInstance); for (const errorHandlerUri in errorHandlers) { - const successObjects = errorHandlers[errorHandlerUri].success?.(normalizedOutput[instanceLocation], instance, localization, ast) ?? []; + const successObjects = errorHandlers[errorHandlerUri].success?.(describedOutput[instanceLocation], instance, localization, ast) ?? []; successes.push(...successObjects); } } @@ -732,53 +774,3 @@ export const getSiblingKeywordLocation = (ast, schemaLocation, siblingKeywordUri return node[1]; }; - -/** - * @overload - * @param {string} schemaUri - * @returns {Promise} - * - * @overload - * @param {string} schemaUri - * @param {API.Json} instance - * @param {API.JsonSchemaErrorsOptions} [options] - * @returns {Promise} - * - * @param {string} schemaUri - * @param {API.Json} instance - * @param {API.JsonSchemaErrorsOptions} [options] - */ -export const validate = async (schemaUri, instance, options) => { - const schema = await getSchema(schemaUri); - const compiledSchema = await compile(schema); - - if (instance === undefined) { - /** @type API.EvaluateInstance */ - return (instance, options) => { - return evaluateCompiledSchema(compiledSchema, instance, options); - }; - } else { - return evaluateCompiledSchema(compiledSchema, instance, options); - } -}; - -/** @type API.evaluateCompiledSchema */ -export const evaluateCompiledSchema = (compiledSchema, instance, options = {}) => { - const localization = Localization.forLocale(options.locale ?? "en-US"); - const jsonNode = Instance.fromJs(instance); - const outputPlugin = new JsonSchemaErrorsOutputPlugin(); - const context = { - ast: compiledSchema.ast, - plugins: [...compiledSchema.ast.plugins, outputPlugin, ...options.plugins ?? []] - }; - const valid = Validation.interpret(compiledSchema.schemaUri, jsonNode, context); - - if (valid) { - return { valid }; - } else { - return { - valid, - errors: getErrors(outputPlugin.output, jsonNode, localization, compiledSchema.ast) - }; - } -}; diff --git a/src/normalization-handlers/additionalProperties.js b/src/normalization-handlers/additionalProperties.js index 6593298..9e9dad5 100644 --- a/src/normalization-handlers/additionalProperties.js +++ b/src/normalization-handlers/additionalProperties.js @@ -25,8 +25,7 @@ const additionalPropertiesNormalizationHandler = { } return outputs; - }, - simpleApplicator: true + } }; export default additionalPropertiesNormalizationHandler; diff --git a/src/normalization-handlers/allOf.js b/src/normalization-handlers/allOf.js index da5a787..6ea83ab 100644 --- a/src/normalization-handlers/allOf.js +++ b/src/normalization-handlers/allOf.js @@ -15,8 +15,7 @@ const allOfNormalizationHandler = { } return outputs; - }, - simpleApplicator: true + } }; export default allOfNormalizationHandler; diff --git a/src/normalization-handlers/dependentSchemas.js b/src/normalization-handlers/dependentSchemas.js index f3e80ad..8825645 100644 --- a/src/normalization-handlers/dependentSchemas.js +++ b/src/normalization-handlers/dependentSchemas.js @@ -24,7 +24,7 @@ const dependentSchemaNormalizationHandler = { return outputs; }, - validityFromSubschemas: true + conditional: true }; export default dependentSchemaNormalizationHandler; diff --git a/src/normalization-handlers/draft-04/additionalItems.js b/src/normalization-handlers/draft-04/additionalItems.js index 0a85eb7..c9d1497 100644 --- a/src/normalization-handlers/draft-04/additionalItems.js +++ b/src/normalization-handlers/draft-04/additionalItems.js @@ -21,8 +21,7 @@ const additionalItemsNormalizationHandler = { } return outputs; - }, - simpleApplicator: true + } }; export default additionalItemsNormalizationHandler; diff --git a/src/normalization-handlers/draft-04/items.js b/src/normalization-handlers/draft-04/items.js index 8ae00d2..bcc1101 100644 --- a/src/normalization-handlers/draft-04/items.js +++ b/src/normalization-handlers/draft-04/items.js @@ -29,8 +29,7 @@ const itemsDraft04NormalizationHandler = { } return outputs; - }, - simpleApplicator: true + } }; export default itemsDraft04NormalizationHandler; diff --git a/src/normalization-handlers/dynamicRef.js b/src/normalization-handlers/dynamicRef.js index 8920a5d..bfb7ea3 100644 --- a/src/normalization-handlers/dynamicRef.js +++ b/src/normalization-handlers/dynamicRef.js @@ -19,8 +19,7 @@ const dynamicRefNormalizationHandler = { } else { return [evaluateSchema(ref, instance, context)]; } - }, - simpleApplicator: true + } }; export default dynamicRefNormalizationHandler; diff --git a/src/normalization-handlers/else.js b/src/normalization-handlers/else.js index 6defa14..3376c49 100644 --- a/src/normalization-handlers/else.js +++ b/src/normalization-handlers/else.js @@ -9,7 +9,7 @@ const elseNormalizationHandler = { evaluate([, elseLocation], instance, context) { return [evaluateSchema(elseLocation, instance, context)]; }, - validityFromSubschemas: true + conditional: true }; export default elseNormalizationHandler; diff --git a/src/normalization-handlers/items.js b/src/normalization-handlers/items.js index 57f4dfa..1ea6e27 100644 --- a/src/normalization-handlers/items.js +++ b/src/normalization-handlers/items.js @@ -21,8 +21,7 @@ const itemsNormalizationHandler = { } return outputs; - }, - simpleApplicator: true + } }; export default itemsNormalizationHandler; diff --git a/src/normalization-handlers/patternProperties.js b/src/normalization-handlers/patternProperties.js index 0181f93..e1fe699 100644 --- a/src/normalization-handlers/patternProperties.js +++ b/src/normalization-handlers/patternProperties.js @@ -25,8 +25,7 @@ const patternPropertiesNormalizationHandler = { } return outputs; - }, - simpleApplicator: true + } }; export default patternPropertiesNormalizationHandler; diff --git a/src/normalization-handlers/prefixItems.js b/src/normalization-handlers/prefixItems.js index 2fc5925..8641dde 100644 --- a/src/normalization-handlers/prefixItems.js +++ b/src/normalization-handlers/prefixItems.js @@ -23,8 +23,7 @@ const prefixItemsNormalizationHandler = { } return outputs; - }, - simpleApplicator: true + } }; export default prefixItemsNormalizationHandler; diff --git a/src/normalization-handlers/properties.js b/src/normalization-handlers/properties.js index 6c443c6..e6dc7d2 100644 --- a/src/normalization-handlers/properties.js +++ b/src/normalization-handlers/properties.js @@ -24,8 +24,7 @@ const propertiesNormalizationHandler = { } return outputs; - }, - simpleApplicator: true + } }; export default propertiesNormalizationHandler; diff --git a/src/normalization-handlers/propertyNames.js b/src/normalization-handlers/propertyNames.js index de49b66..1e3a473 100644 --- a/src/normalization-handlers/propertyNames.js +++ b/src/normalization-handlers/propertyNames.js @@ -20,8 +20,7 @@ const propertyNamesNormalizationHandler = { } return outputs; - }, - simpleApplicator: true + } }; export default propertyNamesNormalizationHandler; diff --git a/src/normalization-handlers/ref.js b/src/normalization-handlers/ref.js index 479acdb..cf00a63 100644 --- a/src/normalization-handlers/ref.js +++ b/src/normalization-handlers/ref.js @@ -8,8 +8,7 @@ import { evaluateSchema } from "../json-schema-errors.js"; const refNormalizationHandler = { evaluate(ref, instance, context) { return [evaluateSchema(ref, instance, context)]; - }, - simpleApplicator: true + } }; export default refNormalizationHandler; diff --git a/src/normalization-handlers/then.js b/src/normalization-handlers/then.js index c56e95a..1c7c468 100644 --- a/src/normalization-handlers/then.js +++ b/src/normalization-handlers/then.js @@ -9,7 +9,7 @@ const thenNormalizationHandler = { evaluate([, then], instance, context) { return [evaluateSchema(then, instance, context)]; }, - validityFromSubschemas: true + conditional: true }; export default thenNormalizationHandler; diff --git a/src/normalization-handlers/unevaluatedItems.js b/src/normalization-handlers/unevaluatedItems.js index 4511b0e..ae177be 100644 --- a/src/normalization-handlers/unevaluatedItems.js +++ b/src/normalization-handlers/unevaluatedItems.js @@ -20,8 +20,7 @@ const unevaluatedItemsNormalizationHandler = { } return outputs; - }, - simpleApplicator: true + } }; export default unevaluatedItemsNormalizationHandler; diff --git a/src/normalization-handlers/unevaluatedProperties.js b/src/normalization-handlers/unevaluatedProperties.js index 94b866d..e749876 100644 --- a/src/normalization-handlers/unevaluatedProperties.js +++ b/src/normalization-handlers/unevaluatedProperties.js @@ -20,8 +20,7 @@ const unevaluatedPropertiesNormalizationHandler = { } return outputs; - }, - simpleApplicator: true + } }; export default unevaluatedPropertiesNormalizationHandler; diff --git a/src/output-format.js b/src/output-format.js new file mode 100644 index 0000000..e78ed32 --- /dev/null +++ b/src/output-format.js @@ -0,0 +1,24 @@ +import { setOutputFormat } from "@hyperjump/json-schema/experimental"; +import { getErrors } from "./json-schema-errors.js"; +import { Localization } from "./localization.js"; +import { JsonSchemaErrorsOutputPlugin } from "./output-plugin.js"; + +/** + * @import { OutputFormatHandler } from "@hyperjump/json-schema/experimental" + */ + +export const JSE = "JSE"; + +/** @type OutputFormatHandler */ +const jseOutputFormatHandler = (options) => { + const plugin = new JsonSchemaErrorsOutputPlugin(); + return { + plugin, + getErrors: (instance, context) => { + const localization = Localization.forLocale(options.locale ?? "en-US"); + return getErrors(plugin.output, instance, localization, context.ast); + } + }; +}; + +setOutputFormat(JSE, jseOutputFormatHandler); diff --git a/src/output-plugin.js b/src/output-plugin.js index 61b74b5..85041b5 100644 --- a/src/output-plugin.js +++ b/src/output-plugin.js @@ -1,5 +1,4 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; -import { isSimpleApplicator } from "./json-schema-errors.js"; /** * @import { EvaluationPlugin, ValidationContext } from "@hyperjump/json-schema/experimental" @@ -30,15 +29,15 @@ export class JsonSchemaErrorsOutputPlugin { context.output = schemaContext.output; } + // Each keyword's result includes the output of its subschemas so the + // subschemas can be described. The results of a simple applicator's + // subschemas are also merged into the results of its parent schema. + /** @type NonNullable["afterKeyword"]> */ - afterKeyword(keywordNode, instance, context, valid, schemaContext) { + afterKeyword(keywordNode, instance, context, valid, schemaContext, keyword) { const [keywordUri, schemaLocation] = keywordNode; - // Whether the result is merged with its parent depends on how this package - // models the keyword rather than the validator. For example, 'if' never - // fails, but its result isn't a requirement of its parent. - const isSimple = isSimpleApplicator(keywordUri); - if (isSimple) { + if (keyword.simpleApplicator) { for (const subSchemaOutput of context.subSchemaOutput ?? []) { mergeOutput(schemaContext.output, subSchemaOutput); } @@ -46,7 +45,7 @@ export class JsonSchemaErrorsOutputPlugin { schemaContext.output[Instance.uri(instance)] ??= {}; schemaContext.output[Instance.uri(instance)][keywordUri] ??= {}; - schemaContext.output[Instance.uri(instance)][keywordUri][schemaLocation] = !isSimple && context.subSchemaOutput + schemaContext.output[Instance.uri(instance)][keywordUri][schemaLocation] = context.subSchemaOutput ? { valid, outputs: context.subSchemaOutput } : { valid }; } diff --git a/src/output-plugin.test.ts b/src/output-plugin.test.ts index 390774e..7c0f05f 100644 --- a/src/output-plugin.test.ts +++ b/src/output-plugin.test.ts @@ -1,6 +1,5 @@ import { afterEach, describe, expect, test } from "vitest"; import { validate, registerSchema, unregisterSchema } from "@hyperjump/json-schema"; -import "./index.js"; import { JsonSchemaErrorsOutputPlugin } from "./output-plugin.js"; describe("JSON Schema Errors Output Format", () => { @@ -28,7 +27,18 @@ describe("JSON Schema Errors Output Format", () => { [`${schemaUri}#/$defs/string/type`]: { valid: false } }, "https://json-schema.org/keyword/ref": { - [`${schemaUri}#/$ref`]: { valid: false } + [`${schemaUri}#/$ref`]: { + valid: false, + outputs: [ + { + "#": { + "https://json-schema.org/keyword/type": { + [`${schemaUri}#/$defs/string/type`]: { valid: false } + } + } + } + ] + } }, "https://json-schema.org/keyword/definitions": { [`${schemaUri}#/$defs`]: { valid: true } @@ -53,7 +63,18 @@ describe("JSON Schema Errors Output Format", () => { [`${schemaUri}#/$defs/string/type`]: { valid: true } }, "https://json-schema.org/keyword/ref": { - [`${schemaUri}#/$ref`]: { valid: true } + [`${schemaUri}#/$ref`]: { + valid: true, + outputs: [ + { + "#": { + "https://json-schema.org/keyword/type": { + [`${schemaUri}#/$defs/string/type`]: { valid: true } + } + } + } + ] + } }, "https://json-schema.org/keyword/definitions": { [`${schemaUri}#/$defs`]: { valid: true } @@ -77,7 +98,18 @@ describe("JSON Schema Errors Output Format", () => { }, "#": { "https://json-schema.org/keyword/additionalProperties": { - [`${schemaUri}#/additionalProperties`]: { valid: false } + [`${schemaUri}#/additionalProperties`]: { + valid: false, + outputs: [ + { + "#/foo": { + "https://json-schema.org/validation": { + [`${schemaUri}#/additionalProperties`]: { valid: false } + } + } + } + ] + } } } }); @@ -101,7 +133,25 @@ describe("JSON Schema Errors Output Format", () => { }, "#": { "https://json-schema.org/keyword/additionalProperties": { - [`${schemaUri}#/additionalProperties`]: { valid: false } + [`${schemaUri}#/additionalProperties`]: { + valid: false, + outputs: [ + { + "#/foo": { + "https://json-schema.org/validation": { + [`${schemaUri}#/additionalProperties`]: { valid: false } + } + } + }, + { + "#/bar": { + "https://json-schema.org/validation": { + [`${schemaUri}#/additionalProperties`]: { valid: false } + } + } + } + ] + } } } }); @@ -122,7 +172,18 @@ describe("JSON Schema Errors Output Format", () => { }, "#": { "https://json-schema.org/keyword/additionalProperties": { - [`${schemaUri}#/additionalProperties`]: { valid: false } + [`${schemaUri}#/additionalProperties`]: { + valid: false, + outputs: [ + { + "#/foo": { + "https://json-schema.org/keyword/type": { + [`${schemaUri}#/additionalProperties/type`]: { valid: false } + } + } + } + ] + } } } }); @@ -163,7 +224,25 @@ describe("JSON Schema Errors Output Format", () => { [`${schemaUri}#/allOf/1/maximum`]: { valid: false } }, "https://json-schema.org/keyword/allOf": { - [`${schemaUri}#/allOf`]: { valid: false } + [`${schemaUri}#/allOf`]: { + valid: false, + outputs: [ + { + "#": { + "https://json-schema.org/keyword/type": { + [`${schemaUri}#/allOf/0/type`]: { valid: true } + } + } + }, + { + "#": { + "https://json-schema.org/keyword/maximum": { + [`${schemaUri}#/allOf/1/maximum`]: { valid: false } + } + } + } + ] + } } } }); @@ -190,7 +269,25 @@ describe("JSON Schema Errors Output Format", () => { [`${schemaUri}#/allOf/1/maximum`]: { valid: false } }, "https://json-schema.org/keyword/allOf": { - [`${schemaUri}#/allOf`]: { valid: false } + [`${schemaUri}#/allOf`]: { + valid: false, + outputs: [ + { + "#": { + "https://json-schema.org/keyword/maximum": { + [`${schemaUri}#/allOf/0/maximum`]: { valid: false } + } + } + }, + { + "#": { + "https://json-schema.org/keyword/maximum": { + [`${schemaUri}#/allOf/1/maximum`]: { valid: false } + } + } + } + ] + } } } }); @@ -215,7 +312,25 @@ describe("JSON Schema Errors Output Format", () => { [`${schemaUri}#/allOf/1/maximum`]: { valid: true } }, "https://json-schema.org/keyword/allOf": { - [`${schemaUri}#/allOf`]: { valid: true } + [`${schemaUri}#/allOf`]: { + valid: true, + outputs: [ + { + "#": { + "https://json-schema.org/keyword/type": { + [`${schemaUri}#/allOf/0/type`]: { valid: true } + } + } + }, + { + "#": { + "https://json-schema.org/keyword/maximum": { + [`${schemaUri}#/allOf/1/maximum`]: { valid: true } + } + } + } + ] + } } } }); @@ -512,6 +627,9 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { + "https://json-schema.org/keyword/required": { + [`${schemaUri}#/dependentSchemas/foo/required`]: { valid: false } + }, "https://json-schema.org/keyword/dependentSchemas": { [`${schemaUri}#/dependentSchemas`]: { valid: false, @@ -542,6 +660,10 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { + "https://json-schema.org/keyword/required": { + [`${schemaUri}#/dependentSchemas/foo/required`]: { valid: false }, + [`${schemaUri}#/dependentSchemas/bar/required`]: { valid: false } + }, "https://json-schema.org/keyword/dependentSchemas": { [`${schemaUri}#/dependentSchemas`]: { valid: false, @@ -578,6 +700,9 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { + "https://json-schema.org/keyword/required": { + [`${schemaUri}#/dependentSchemas/foo/required`]: { valid: true } + }, "https://json-schema.org/keyword/dependentSchemas": { [`${schemaUri}#/dependentSchemas`]: { valid: true, @@ -622,6 +747,9 @@ describe("JSON Schema Errors Output Format", () => { ] } }, + "https://json-schema.org/keyword/minLength": { + [`${schemaUri}#/then/minLength`]: { valid: false } + }, "https://json-schema.org/keyword/then": { [`${schemaUri}#/then`]: { valid: false, @@ -664,6 +792,9 @@ describe("JSON Schema Errors Output Format", () => { ] } }, + "https://json-schema.org/keyword/minLength": { + [`${schemaUri}#/then/minLength`]: { valid: true } + }, "https://json-schema.org/keyword/then": { [`${schemaUri}#/then`]: { valid: true, @@ -712,6 +843,9 @@ describe("JSON Schema Errors Output Format", () => { ] } }, + "https://json-schema.org/keyword/minimum": { + [`${schemaUri}#/else/minimum`]: { valid: false } + }, "https://json-schema.org/keyword/else": { [`${schemaUri}#/else`]: { valid: false, @@ -758,6 +892,9 @@ describe("JSON Schema Errors Output Format", () => { ] } }, + "https://json-schema.org/keyword/minimum": { + [`${schemaUri}#/else/minimum`]: { valid: true } + }, "https://json-schema.org/keyword/else": { [`${schemaUri}#/else`]: { valid: true, @@ -798,7 +935,25 @@ describe("JSON Schema Errors Output Format", () => { }, "#": { "https://json-schema.org/keyword/items": { - [`${schemaUri}#/items`]: { valid: false } + [`${schemaUri}#/items`]: { + valid: false, + outputs: [ + { + "#/0": { + "https://json-schema.org/keyword/type": { + [`${schemaUri}#/items/type`]: { valid: false } + } + } + }, + { + "#/1": { + "https://json-schema.org/keyword/type": { + [`${schemaUri}#/items/type`]: { valid: false } + } + } + } + ] + } } } }); @@ -819,7 +974,18 @@ describe("JSON Schema Errors Output Format", () => { }, "#": { "https://json-schema.org/keyword/items": { - [`${schemaUri}#/items`]: { valid: true } + [`${schemaUri}#/items`]: { + valid: true, + outputs: [ + { + "#/0": { + "https://json-schema.org/keyword/type": { + [`${schemaUri}#/items/type`]: { valid: true } + } + } + } + ] + } } } }); @@ -850,7 +1016,25 @@ describe("JSON Schema Errors Output Format", () => { }, "#": { "https://json-schema.org/keyword/patternProperties": { - [`${schemaUri}#/patternProperties`]: { valid: false } + [`${schemaUri}#/patternProperties`]: { + valid: false, + outputs: [ + { + "#/foo": { + "https://json-schema.org/keyword/type": { + [`${schemaUri}#/patternProperties/%5Ef/type`]: { valid: false } + } + } + }, + { + "#/bar": { + "https://json-schema.org/keyword/type": { + [`${schemaUri}#/patternProperties/%5Eb/type`]: { valid: false } + } + } + } + ] + } } } }); @@ -879,7 +1063,25 @@ describe("JSON Schema Errors Output Format", () => { }, "#": { "https://json-schema.org/keyword/patternProperties": { - [`${schemaUri}#/patternProperties`]: { valid: true } + [`${schemaUri}#/patternProperties`]: { + valid: true, + outputs: [ + { + "#/foo": { + "https://json-schema.org/keyword/type": { + [`${schemaUri}#/patternProperties/%5Ef/type`]: { valid: true } + } + } + }, + { + "#/bar": { + "https://json-schema.org/keyword/type": { + [`${schemaUri}#/patternProperties/%5Eb/type`]: { valid: true } + } + } + } + ] + } } } }); @@ -910,7 +1112,25 @@ describe("JSON Schema Errors Output Format", () => { }, "#": { "https://json-schema.org/keyword/prefixItems": { - [`${schemaUri}#/prefixItems`]: { valid: false } + [`${schemaUri}#/prefixItems`]: { + valid: false, + outputs: [ + { + "#/0": { + "https://json-schema.org/keyword/type": { + [`${schemaUri}#/prefixItems/0/type`]: { valid: false } + } + } + }, + { + "#/1": { + "https://json-schema.org/keyword/type": { + [`${schemaUri}#/prefixItems/1/type`]: { valid: false } + } + } + } + ] + } } } }); @@ -939,7 +1159,25 @@ describe("JSON Schema Errors Output Format", () => { }, "#": { "https://json-schema.org/keyword/prefixItems": { - [`${schemaUri}#/prefixItems`]: { valid: true } + [`${schemaUri}#/prefixItems`]: { + valid: true, + outputs: [ + { + "#/0": { + "https://json-schema.org/keyword/type": { + [`${schemaUri}#/prefixItems/0/type`]: { valid: true } + } + } + }, + { + "#/1": { + "https://json-schema.org/keyword/type": { + [`${schemaUri}#/prefixItems/1/type`]: { valid: true } + } + } + } + ] + } } } }); @@ -970,7 +1208,25 @@ describe("JSON Schema Errors Output Format", () => { }, "#": { "https://json-schema.org/keyword/properties": { - [`${schemaUri}#/properties`]: { valid: false } + [`${schemaUri}#/properties`]: { + valid: false, + outputs: [ + { + "#/foo": { + "https://json-schema.org/keyword/type": { + [`${schemaUri}#/properties/foo/type`]: { valid: false } + } + } + }, + { + "#/bar": { + "https://json-schema.org/keyword/type": { + [`${schemaUri}#/properties/bar/type`]: { valid: false } + } + } + } + ] + } } } }); @@ -999,7 +1255,25 @@ describe("JSON Schema Errors Output Format", () => { }, "#": { "https://json-schema.org/keyword/properties": { - [`${schemaUri}#/properties`]: { valid: true } + [`${schemaUri}#/properties`]: { + valid: true, + outputs: [ + { + "#/foo": { + "https://json-schema.org/keyword/type": { + [`${schemaUri}#/properties/foo/type`]: { valid: true } + } + } + }, + { + "#/bar": { + "https://json-schema.org/keyword/type": { + [`${schemaUri}#/properties/bar/type`]: { valid: true } + } + } + } + ] + } } } }); @@ -1027,7 +1301,25 @@ describe("JSON Schema Errors Output Format", () => { }, "#": { "https://json-schema.org/keyword/propertyNames": { - [`${schemaUri}#/propertyNames`]: { valid: false } + [`${schemaUri}#/propertyNames`]: { + valid: false, + outputs: [ + { + "#*/banana": { + "https://json-schema.org/keyword/pattern": { + [`${schemaUri}#/propertyNames/pattern`]: { valid: false } + } + } + }, + { + "#*/pear": { + "https://json-schema.org/keyword/pattern": { + [`${schemaUri}#/propertyNames/pattern`]: { valid: false } + } + } + } + ] + } } } }); @@ -1048,7 +1340,18 @@ describe("JSON Schema Errors Output Format", () => { }, "#": { "https://json-schema.org/keyword/propertyNames": { - [`${schemaUri}#/propertyNames`]: { valid: true } + [`${schemaUri}#/propertyNames`]: { + valid: true, + outputs: [ + { + "#*/apple": { + "https://json-schema.org/keyword/pattern": { + [`${schemaUri}#/propertyNames/pattern`]: { valid: true } + } + } + } + ] + } } } }); @@ -1069,7 +1372,18 @@ describe("JSON Schema Errors Output Format", () => { }, "#": { "https://json-schema.org/keyword/unevaluatedProperties": { - [`${schemaUri}#/unevaluatedProperties`]: { valid: false } + [`${schemaUri}#/unevaluatedProperties`]: { + valid: false, + outputs: [ + { + "#/foo": { + "https://json-schema.org/validation": { + [`${schemaUri}#/unevaluatedProperties`]: { valid: false } + } + } + } + ] + } } } }); @@ -1094,10 +1408,34 @@ describe("JSON Schema Errors Output Format", () => { }, "#": { "https://json-schema.org/keyword/properties": { - [`${schemaUri}#/properties`]: { valid: false } + [`${schemaUri}#/properties`]: { + valid: false, + outputs: [ + { + }, + { + "#/bar": { + "https://json-schema.org/validation": { + [`${schemaUri}#/properties/bar`]: { valid: false } + } + } + } + ] + } }, "https://json-schema.org/keyword/unevaluatedProperties": { - [`${schemaUri}#/unevaluatedProperties`]: { valid: false } + [`${schemaUri}#/unevaluatedProperties`]: { + valid: false, + outputs: [ + { + "#/baz": { + "https://json-schema.org/validation": { + [`${schemaUri}#/unevaluatedProperties`]: { valid: false } + } + } + } + ] + } } }, "#/baz": { @@ -1123,7 +1461,18 @@ describe("JSON Schema Errors Output Format", () => { }, "#": { "https://json-schema.org/keyword/unevaluatedProperties": { - [`${schemaUri}#/unevaluatedProperties`]: { valid: false } + [`${schemaUri}#/unevaluatedProperties`]: { + valid: false, + outputs: [ + { + "#/foo": { + "https://json-schema.org/keyword/type": { + [`${schemaUri}#/unevaluatedProperties/type`]: { valid: false } + } + } + } + ] + } } } }); @@ -1158,7 +1507,18 @@ describe("JSON Schema Errors Output Format", () => { }, "#": { "https://json-schema.org/keyword/unevaluatedItems": { - [`${schemaUri}#/unevaluatedItems`]: { valid: false } + [`${schemaUri}#/unevaluatedItems`]: { + valid: false, + outputs: [ + { + "#/0": { + "https://json-schema.org/validation": { + [`${schemaUri}#/unevaluatedItems`]: { valid: false } + } + } + } + ] + } } } }); @@ -1179,7 +1539,18 @@ describe("JSON Schema Errors Output Format", () => { }, "#": { "https://json-schema.org/keyword/unevaluatedItems": { - [`${schemaUri}#/unevaluatedItems`]: { valid: false } + [`${schemaUri}#/unevaluatedItems`]: { + valid: false, + outputs: [ + { + "#/0": { + "https://json-schema.org/keyword/type": { + [`${schemaUri}#/unevaluatedItems/type`]: { valid: false } + } + } + } + ] + } } } }); @@ -1201,10 +1572,34 @@ describe("JSON Schema Errors Output Format", () => { }, "#": { "https://json-schema.org/keyword/prefixItems": { - [`${schemaUri}#/prefixItems`]: { valid: false } + [`${schemaUri}#/prefixItems`]: { + valid: false, + outputs: [ + { + }, + { + "#/1": { + "https://json-schema.org/validation": { + [`${schemaUri}#/prefixItems/1`]: { valid: false } + } + } + } + ] + } }, "https://json-schema.org/keyword/unevaluatedItems": { - [`${schemaUri}#/unevaluatedItems`]: { valid: false } + [`${schemaUri}#/unevaluatedItems`]: { + valid: false, + outputs: [ + { + "#/2": { + "https://json-schema.org/validation": { + [`${schemaUri}#/unevaluatedItems`]: { valid: false } + } + } + } + ] + } } }, "#/2": { @@ -1844,7 +2239,18 @@ describe("JSON Schema Errors Output Format", () => { }, "#": { "https://json-schema.org/keyword/properties": { - [`${schemaUri}#/properties`]: { valid: false } + [`${schemaUri}#/properties`]: { + valid: false, + outputs: [ + { + "#/foo": { + "https://json-schema.org/keyword/type": { + [`${schemaUri}#/properties/foo/type`]: { valid: false } + } + } + } + ] + } }, "https://json-schema.org/keyword/required": { [`${schemaUri}#/required`]: { valid: false } @@ -1874,12 +2280,50 @@ describe("JSON Schema Errors Output Format", () => { }, "#/foo": { "https://json-schema.org/keyword/properties": { - [`${schemaUri}#/properties/foo/properties`]: { valid: false } + [`${schemaUri}#/properties/foo/properties`]: { + valid: false, + outputs: [ + { + "#/foo/bar": { + "https://json-schema.org/keyword/type": { + [`${schemaUri}#/properties/foo/properties/bar/type`]: { valid: false } + } + } + } + ] + } } }, "#": { "https://json-schema.org/keyword/properties": { - [`${schemaUri}#/properties`]: { valid: false } + [`${schemaUri}#/properties`]: { + valid: false, + outputs: [ + { + "#/foo/bar": { + "https://json-schema.org/keyword/type": { + [`${schemaUri}#/properties/foo/properties/bar/type`]: { valid: false } + } + }, + "#/foo": { + "https://json-schema.org/keyword/properties": { + [`${schemaUri}#/properties/foo/properties`]: { + valid: false, + outputs: [ + { + "#/foo/bar": { + "https://json-schema.org/keyword/type": { + [`${schemaUri}#/properties/foo/properties/bar/type`]: { valid: false } + } + } + } + ] + } + } + } + } + ] + } } } });