From 7bada89856b6094c07e455c2d6a5cf3fcefd9809 Mon Sep 17 00:00:00 2001 From: Jason Desrosiers Date: Tue, 20 Oct 2026 22:32:20 -0700 Subject: [PATCH 1/4] Compute each output location's URI once The output plugin built the instance location's URI three times for every keyword, and building it encodes the pointer. Validating a 150KB GitHub workflow against its schema with JSE output went from about 44ms to 30ms. Co-Authored-By: Claude Opus 5.5 --- src/output-plugin.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/output-plugin.js b/src/output-plugin.js index 7db0c9a..bd0de4c 100644 --- a/src/output-plugin.js +++ b/src/output-plugin.js @@ -36,9 +36,9 @@ export class JsonSchemaErrorsOutputPlugin { afterKeyword(keywordNode, instance, context, valid, schemaContext) { const [keywordUri, schemaLocation, value] = keywordNode; - schemaContext.output[Instance.uri(instance)] ??= {}; - schemaContext.output[Instance.uri(instance)][keywordUri] ??= {}; - schemaContext.output[Instance.uri(instance)][keywordUri][schemaLocation] = context.subSchemaOutput + const instanceOutput = schemaContext.output[Instance.uri(instance)] ??= {}; + const keywordOutput = instanceOutput[keywordUri] ??= {}; + keywordOutput[schemaLocation] = context.subSchemaOutput ? { valid, value, outputs: context.subSchemaOutput } : { valid, value }; } @@ -46,9 +46,9 @@ export class JsonSchemaErrorsOutputPlugin { /** @type NonNullable["afterSchema"]> */ afterSchema(url, instance, context, valid) { if (typeof context.ast[url] === "boolean" && !valid) { - context.output[Instance.uri(instance)] ??= {}; - context.output[Instance.uri(instance)]["https://json-schema.org/validation"] ??= {}; - context.output[Instance.uri(instance)]["https://json-schema.org/validation"][url] = { valid, value: false }; + const instanceOutput = context.output[Instance.uri(instance)] ??= {}; + const validationOutput = instanceOutput["https://json-schema.org/validation"] ??= {}; + validationOutput[url] = { valid, value: false }; } context.subSchemaOutput ??= []; From 5587b0f456a1ab527d63e1fede39b48d1ad566ac Mon Sep 17 00:00:00 2001 From: Jason Desrosiers Date: Tue, 20 Oct 2026 22:33:36 -0700 Subject: [PATCH 2/4] Build output records without hidden classes Output records are keyed by instance locations, keyword URIs and schema locations, so nearly every key added to a plain object gave it a new hidden class. Records without a prototype start in dictionary mode instead. Validating a 150KB GitHub workflow against its schema with JSE output went from about 30ms to 26ms. Co-Authored-By: Claude Opus 5.5 --- src/output-plugin.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/output-plugin.js b/src/output-plugin.js index bd0de4c..15e0fd0 100644 --- a/src/output-plugin.js +++ b/src/output-plugin.js @@ -21,7 +21,7 @@ export class JsonSchemaErrorsOutputPlugin { /** @type NonNullable["beforeSchema"]> */ beforeSchema(_url, _instance, context) { - context.output = {}; + context.output = Object.create(null); } /** @type NonNullable["beforeKeyword"]> */ @@ -36,8 +36,8 @@ export class JsonSchemaErrorsOutputPlugin { afterKeyword(keywordNode, instance, context, valid, schemaContext) { const [keywordUri, schemaLocation, value] = keywordNode; - const instanceOutput = schemaContext.output[Instance.uri(instance)] ??= {}; - const keywordOutput = instanceOutput[keywordUri] ??= {}; + const instanceOutput = schemaContext.output[Instance.uri(instance)] ??= Object.create(null); + const keywordOutput = instanceOutput[keywordUri] ??= Object.create(null); keywordOutput[schemaLocation] = context.subSchemaOutput ? { valid, value, outputs: context.subSchemaOutput } : { valid, value }; @@ -46,8 +46,8 @@ export class JsonSchemaErrorsOutputPlugin { /** @type NonNullable["afterSchema"]> */ afterSchema(url, instance, context, valid) { if (typeof context.ast[url] === "boolean" && !valid) { - const instanceOutput = context.output[Instance.uri(instance)] ??= {}; - const validationOutput = instanceOutput["https://json-schema.org/validation"] ??= {}; + const instanceOutput = context.output[Instance.uri(instance)] ??= Object.create(null); + const validationOutput = instanceOutput["https://json-schema.org/validation"] ??= Object.create(null); validationOutput[url] = { valid, value: false }; } From 4e4ed2d4665231b4c874e2509b70bf9513b3909c Mon Sep 17 00:00:00 2001 From: Jason Desrosiers Date: Tue, 20 Oct 2026 22:33:36 -0700 Subject: [PATCH 3/4] Compute the instance location's URI once per schema All of a schema's keywords apply to the same instance location, but its URI was built after every keyword. It's now built when the schema is entered. Validating a 150KB GitHub workflow against its schema with JSE output went from about 26ms to 22ms. Co-Authored-By: Claude Opus 5.5 --- src/output-plugin.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/output-plugin.js b/src/output-plugin.js index 15e0fd0..1309a23 100644 --- a/src/output-plugin.js +++ b/src/output-plugin.js @@ -8,6 +8,7 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; /** * @typedef {ValidationContext & { * output: NormalizedOutput; + * instanceUri: string; * subSchemaOutput?: NormalizedOutput[]; * }} ErrorsContext */ @@ -20,8 +21,10 @@ export class JsonSchemaErrorsOutputPlugin { } /** @type NonNullable["beforeSchema"]> */ - beforeSchema(_url, _instance, context) { + beforeSchema(_url, instance, context) { context.output = Object.create(null); + // A schema's keywords all apply to the same instance location + context.instanceUri = Instance.uri(instance); } /** @type NonNullable["beforeKeyword"]> */ @@ -33,10 +36,10 @@ export class JsonSchemaErrorsOutputPlugin { // subschemas can be described /** @type NonNullable["afterKeyword"]> */ - afterKeyword(keywordNode, instance, context, valid, schemaContext) { + afterKeyword(keywordNode, _instance, context, valid, schemaContext) { const [keywordUri, schemaLocation, value] = keywordNode; - const instanceOutput = schemaContext.output[Instance.uri(instance)] ??= Object.create(null); + const instanceOutput = schemaContext.output[schemaContext.instanceUri] ??= Object.create(null); const keywordOutput = instanceOutput[keywordUri] ??= Object.create(null); keywordOutput[schemaLocation] = context.subSchemaOutput ? { valid, value, outputs: context.subSchemaOutput } @@ -44,9 +47,9 @@ export class JsonSchemaErrorsOutputPlugin { } /** @type NonNullable["afterSchema"]> */ - afterSchema(url, instance, context, valid) { + afterSchema(url, _instance, context, valid) { if (typeof context.ast[url] === "boolean" && !valid) { - const instanceOutput = context.output[Instance.uri(instance)] ??= Object.create(null); + const instanceOutput = context.output[context.instanceUri] ??= Object.create(null); const validationOutput = instanceOutput["https://json-schema.org/validation"] ??= Object.create(null); validationOutput[url] = { valid, value: false }; } From f21e36f366723c2c642082daf06c0b88192e6118 Mon Sep 17 00:00:00 2001 From: Jason Desrosiers Date: Tue, 20 Oct 2026 22:56:16 -0700 Subject: [PATCH 4/4] Build the normalized output only when it's asked for The output is only used when there are errors, but it was built during every evaluation. The plugin now records what each schema's keywords did and builds the normalized output from that when it's first asked for. Validating a valid 150KB GitHub workflow against its schema with JSE output went from about 21ms to 7ms. An invalid one went from about 23ms to 24ms because the output is built after evaluation instead of during it. Co-Authored-By: Claude Opus 5.5 --- src/output-plugin.js | 109 ++++++++++++++++++++++++++++--------------- 1 file changed, 72 insertions(+), 37 deletions(-) diff --git a/src/output-plugin.js b/src/output-plugin.js index 1309a23..036ac62 100644 --- a/src/output-plugin.js +++ b/src/output-plugin.js @@ -1,62 +1,97 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; /** - * @import { EvaluationPlugin, ValidationContext } from "@hyperjump/json-schema/experimental" + * @import { EvaluationPlugin, Node, ValidationContext } from "@hyperjump/json-schema/experimental" + * @import { JsonNode } from "@hyperjump/json-schema/instance/experimental" * @import { NormalizedOutput } from "./index.js" */ +/** + * What evaluating a schema did. Each keyword's result includes the results of + * its subschemas so the subschemas can be described. + * + * @typedef {{ + * url: string; + * instance: JsonNode; + * valid: boolean; + * isBoolean: boolean; + * keywords: KeywordResult[]; + * }} SchemaResult + * + * @typedef {{ + * node: Node; + * valid: boolean; + * subschemas: SchemaResult[] | undefined; + * }} KeywordResult + */ + /** * @typedef {ValidationContext & { - * output: NormalizedOutput; - * instanceUri: string; - * subSchemaOutput?: NormalizedOutput[]; + * schemaResult: SchemaResult; + * subschemaResults?: SchemaResult[]; * }} ErrorsContext */ -/** @implements EvaluationPlugin */ +/** + * The output is only needed when there are errors, so evaluation records what + * happened and the normalized output is built from it when it's asked for. + * + * @implements EvaluationPlugin + */ export class JsonSchemaErrorsOutputPlugin { - constructor() { - /** @type NormalizedOutput */ - this.output = {}; - } + /** @type SchemaResult | undefined */ + #result; + /** @type NormalizedOutput | undefined */ + #output; - /** @type NonNullable["beforeSchema"]> */ - beforeSchema(_url, instance, context) { - context.output = Object.create(null); - // A schema's keywords all apply to the same instance location - context.instanceUri = Instance.uri(instance); + /** @type NormalizedOutput */ + get output() { + this.#output ??= this.#result ? toNormalizedOutput(this.#result) : {}; + return this.#output; } - /** @type NonNullable["beforeKeyword"]> */ - beforeKeyword(_node, _instance, context, schemaContext) { - context.output = schemaContext.output; + /** @type NonNullable["beforeSchema"]> */ + beforeSchema(url, instance, context) { + context.schemaResult = { url, instance, valid: true, isBoolean: typeof context.ast[url] === "boolean", keywords: [] }; } - // Each keyword's result includes the output of its subschemas so the - // subschemas can be described - /** @type NonNullable["afterKeyword"]> */ - afterKeyword(keywordNode, _instance, context, valid, schemaContext) { - const [keywordUri, schemaLocation, value] = keywordNode; - - const instanceOutput = schemaContext.output[schemaContext.instanceUri] ??= Object.create(null); - const keywordOutput = instanceOutput[keywordUri] ??= Object.create(null); - keywordOutput[schemaLocation] = context.subSchemaOutput - ? { valid, value, outputs: context.subSchemaOutput } - : { valid, value }; + afterKeyword(node, _instance, context, valid, schemaContext) { + schemaContext.schemaResult.keywords.push({ node, valid, subschemas: context.subschemaResults }); } /** @type NonNullable["afterSchema"]> */ - afterSchema(url, _instance, context, valid) { - if (typeof context.ast[url] === "boolean" && !valid) { - const instanceOutput = context.output[context.instanceUri] ??= Object.create(null); - const validationOutput = instanceOutput["https://json-schema.org/validation"] ??= Object.create(null); - validationOutput[url] = { valid, value: false }; - } + afterSchema(_url, _instance, context, valid) { + context.schemaResult.valid = valid; - context.subSchemaOutput ??= []; - context.subSchemaOutput.push(context.output); + context.subschemaResults ??= []; + context.subschemaResults.push(context.schemaResult); - this.output = context.output; + this.#result = context.schemaResult; + this.#output = undefined; } } + +/** @type (schemaResult: SchemaResult) => NormalizedOutput */ +const toNormalizedOutput = ({ url, instance, valid, isBoolean, keywords }) => { + /** @type NormalizedOutput */ + const output = Object.create(null); + + if (isBoolean) { + if (!valid) { + output[Instance.uri(instance)] = { + "https://json-schema.org/validation": { [url]: { valid, value: false } } + }; + } + } else if (keywords.length > 0) { + const instanceOutput = output[Instance.uri(instance)] = Object.create(null); + for (const { node: [keywordUri, schemaLocation, value], valid, subschemas } of keywords) { + instanceOutput[keywordUri] ??= Object.create(null); + instanceOutput[keywordUri][schemaLocation] = subschemas + ? { valid, value, outputs: subschemas.map(toNormalizedOutput) } + : { valid, value }; + } + } + + return output; +};