Skip to content

Instantly share code, notes, and snippets.

@jdesrosiers
Last active November 22, 2025 22:46
Show Gist options
  • Select an option

  • Save jdesrosiers/f16e94fa05a2c4879ca66b968b07c6b4 to your computer and use it in GitHub Desktop.

Select an option

Save jdesrosiers/f16e94fa05a2c4879ca66b968b07c6b4 to your computer and use it in GitHub Desktop.
Create a normalized identifier for JSON Schema Test Suite tests
import * as Schema from "@hyperjump/browser";
import * as Pact from "@hyperjump/pact";
import * as JsonPointer from "@hyperjump/json-pointer";
import { toAbsoluteIri } from "@hyperjump/uri";
import { registerSchema, unregisterSchema } from "@hyperjump/json-schema/draft-2020-12";
import { getSchema, getKeywordId } from "@hyperjump/json-schema/experimental";
import "@hyperjump/json-schema/draft-2019-09";
import "@hyperjump/json-schema/draft-07";
import "@hyperjump/json-schema/draft-06";
import "@hyperjump/json-schema/draft-04";
export const normalize = async (rawSchema, dialectUri) => {
const schemaUri = "https://test-suite.json-schema.org/main";
try {
registerSchema(rawSchema, schemaUri, dialectUri);
const schema = await getSchema(schemaUri);
const ast = { metaData: {} };
await compile(schema, ast);
return ast;
} finally {
unregisterSchema(schemaUri);
}
};
const compile = async (schema, ast) => {
if (!(schema.document.baseUri in ast.metaData)) {
ast.metaData[schema.document.baseUri] = {
anchors: schema.document.anchors,
dynamicAnchors: schema.document.dynamicAnchors
};
}
const url = canonicalUri(schema);
if (!(url in ast)) {
const schemaValue = Schema.value(schema);
if (!["object", "boolean"].includes(typeof schemaValue)) {
throw Error(`No schema found at '${url}'`);
}
if (typeof schemaValue === "boolean") {
ast[url] = schemaValue;
} else {
ast[url] = [];
for await (const [keyword, keywordSchema] of Schema.entries(schema)) {
const keywordUri = getKeywordId(keyword, schema.document.dialectId);
if (!keywordUri || keywordUri === "https://json-schema.org/keyword/comment") {
continue;
}
ast[url].push({
keyword: keywordUri,
location: JsonPointer.append(keyword, canonicalUri(schema)),
value: await getKeywordHandler(keywordUri)(keywordSchema, ast, schema)
});
}
}
}
return url;
};
const canonicalUri = (schema) => `${schema.document.baseUri}#${encodeURI(schema.cursor)}`;
const getKeywordHandler = (keywordUri) => {
if (keywordUri in keywordHandlers) {
return keywordHandlers[keywordUri];
} else if (keywordUri.startsWith("https://json-schema.org/keyword/unknown#")) {
return keywordHandlers["https://json-schema.org/keyword/unknown"];
} else {
throw Error(`Missing handler for keyword: ${keywordUri}`);
}
};
const simpleValue = (keyword) => Schema.value(keyword);
const simpleApplicator = (keyword, ast) => compile(keyword, ast);
const objectApplicator = (keyword, ast) => {
return Pact.pipe(
Schema.entries(keyword),
Pact.asyncMap(async ([propertyName, subSchema]) => [propertyName, await compile(subSchema, ast)]),
Pact.asyncCollectObject
);
};
const arrayApplicator = (keyword, ast) => {
return Pact.pipe(
Schema.iter(keyword),
Pact.asyncMap(async (subSchema) => await compile(subSchema, ast)),
Pact.asyncCollectArray
);
};
const keywordHandlers = {
"https://json-schema.org/keyword/additionalProperties": simpleApplicator,
"https://json-schema.org/keyword/allOf": arrayApplicator,
"https://json-schema.org/keyword/anyOf": arrayApplicator,
"https://json-schema.org/keyword/const": simpleValue,
"https://json-schema.org/keyword/contains": simpleApplicator,
"https://json-schema.org/keyword/contentEncoding": simpleValue,
"https://json-schema.org/keyword/contentMediaType": simpleValue,
"https://json-schema.org/keyword/contentSchema": simpleApplicator,
"https://json-schema.org/keyword/default": simpleValue,
"https://json-schema.org/keyword/definitions": objectApplicator,
"https://json-schema.org/keyword/dependentRequired": simpleValue,
"https://json-schema.org/keyword/dependentSchemas": objectApplicator,
"https://json-schema.org/keyword/deprecated": simpleValue,
"https://json-schema.org/keyword/description": simpleValue,
"https://json-schema.org/keyword/dynamicRef": simpleValue,
"https://json-schema.org/keyword/else": simpleApplicator,
"https://json-schema.org/keyword/enum": simpleValue,
"https://json-schema.org/keyword/examples": simpleValue,
"https://json-schema.org/keyword/exclusiveMaximum": simpleValue,
"https://json-schema.org/keyword/exclusiveMinimum": simpleValue,
"https://json-schema.org/keyword/if": simpleApplicator,
"https://json-schema.org/keyword/items": simpleApplicator,
"https://json-schema.org/keyword/maxContains": simpleValue,
"https://json-schema.org/keyword/maxItems": simpleValue,
"https://json-schema.org/keyword/maxLength": simpleValue,
"https://json-schema.org/keyword/maxProperties": simpleValue,
"https://json-schema.org/keyword/maximum": simpleValue,
"https://json-schema.org/keyword/minContains": simpleValue,
"https://json-schema.org/keyword/minItems": simpleValue,
"https://json-schema.org/keyword/minLength": simpleValue,
"https://json-schema.org/keyword/minProperties": simpleValue,
"https://json-schema.org/keyword/minimum": simpleValue,
"https://json-schema.org/keyword/multipleOf": simpleValue,
"https://json-schema.org/keyword/not": simpleApplicator,
"https://json-schema.org/keyword/oneOf": arrayApplicator,
"https://json-schema.org/keyword/pattern": simpleValue,
"https://json-schema.org/keyword/patternProperties": objectApplicator,
"https://json-schema.org/keyword/prefixItems": arrayApplicator,
"https://json-schema.org/keyword/properties": objectApplicator,
"https://json-schema.org/keyword/propertyNames": simpleApplicator,
"https://json-schema.org/keyword/readOnly": simpleValue,
"https://json-schema.org/keyword/ref": compile,
"https://json-schema.org/keyword/required": simpleValue,
"https://json-schema.org/keyword/title": simpleValue,
"https://json-schema.org/keyword/then": simpleApplicator,
"https://json-schema.org/keyword/type": simpleValue,
"https://json-schema.org/keyword/unevaluatedItems": simpleApplicator,
"https://json-schema.org/keyword/unevaluatedProperties": simpleApplicator,
"https://json-schema.org/keyword/uniqueItems": simpleValue,
"https://json-schema.org/keyword/unknown": simpleValue,
"https://json-schema.org/keyword/writeOnly": simpleValue,
"https://json-schema.org/keyword/draft-2020-12/format": simpleValue,
"https://json-schema.org/keyword/draft-2020-12/format-assertion": simpleValue,
"https://json-schema.org/keyword/draft-2019-09/formatAssertion": simpleValue,
"https://json-schema.org/keyword/draft-2019-09/format": simpleValue,
"https://json-schema.org/keyword/draft-07/format": simpleValue,
"https://json-schema.org/keyword/draft-06/contains": simpleApplicator,
"https://json-schema.org/keyword/draft-06/format": simpleValue,
"https://json-schema.org/keyword/draft-04/additionalItems": simpleApplicator,
"https://json-schema.org/keyword/draft-04/dependencies": (keyword, ast) => {
return Pact.pipe(
Schema.entries(keyword),
Pact.asyncMap(async ([propertyName, schema]) => {
return [
propertyName,
Schema.typeOf(schema) === "array" ? Schema.value(schema) : await compile(schema, ast)
];
}),
Pact.asyncCollectObject
);
},
"https://json-schema.org/keyword/draft-04/exclusiveMaximum": simpleValue,
"https://json-schema.org/keyword/draft-04/exclusiveMinimum": simpleValue,
"https://json-schema.org/keyword/draft-04/format": simpleValue,
"https://json-schema.org/keyword/draft-04/items": (keyword, ast) => {
return Schema.typeOf(keyword) === "array"
? arrayApplicator(keyword, ast)
: simpleApplicator(keyword, ast);
},
"https://json-schema.org/keyword/draft-04/maximum": simpleValue,
"https://json-schema.org/keyword/draft-04/minimum": simpleValue
};
///////////////////////////////////////////////////////////////////////////////
// Example Usage
///////////////////////////////////////////////////////////////////////////////
import * as crypto from "node:crypto";
import * as fs from "node:fs";
import jsonStringify from "json-stringify-deterministic";
const dialectUri = "https://json-schema.org/draft/2020-12/schema";
// Remotes
const testSuitePath = "./node_modules/json-schema-test-suite";
export const loadRemotes = (dialectId, filePath = `${testSuitePath}/remotes`, url = "") => {
fs.readdirSync(filePath, { withFileTypes: true })
.forEach((entry) => {
if (entry.isFile() && entry.name.endsWith(".json")) {
const remote = JSON.parse(fs.readFileSync(`${filePath}/${entry.name}`, "utf8"));
if (!remote.$schema || toAbsoluteIri(remote.$schema) === dialectId) {
registerSchema(remote, `http://localhost:1234${url}/${entry.name}`, dialectId);
}
} else if (entry.isDirectory()) {
loadRemotes(dialectId, `${filePath}/${entry.name}`, `${url}/${entry.name}`);
}
});
};
loadRemotes(dialectUri);
const testCase = {
description: "Example",
schema: {
$schema: dialectUri,
type: "number"
},
tests: [
{
description: "Test 1",
data: 42,
valid: true
},
{
description: "Test 2",
data: { a: 1, b: 2 },
valid: false
}
]
};
const schema = await normalize(testCase.schema, dialectUri);
for (const test of testCase.tests) {
const testId = crypto.createHash("md5")
.update(jsonStringify(schema) + jsonStringify(test.data) + test.valid)
.digest("hex");
console.log(testId);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment