Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
44fc7b2
Add test IDs to draft2020-12/enum.json using normalized schema hash (…
AnirudhJindal Nov 25, 2025
d2980e4
Allow optional id property on tests in test-schema
AnirudhJindal Nov 25, 2025
8b990ad
Fix script based on reviews
AnirudhJindal Dec 17, 2025
7a2270b
adding jsoc-parser and updating according to the reviews
AnirudhJindal Jan 5, 2026
6cc0f8f
minor changes as per review
AnirudhJindal Jan 8, 2026
7b86302
Remove accidentally committed node_modules
AnirudhJindal Jan 11, 2026
90d4ebc
added a github action for automating addition of test-ids
AnirudhJindal Mar 3, 2026
ab25581
changed the script restore method to the path filtring
AnirudhJindal Mar 6, 2026
a236d61
Add test IDs to draft2020-12/enum.json using normalized schema hash (…
AnirudhJindal Nov 25, 2025
0ae5f26
Allow optional id property on tests in test-schema
AnirudhJindal Nov 25, 2025
44ab305
Fix script based on reviews
AnirudhJindal Dec 17, 2025
927bd9b
adding jsoc-parser and updating according to the reviews
AnirudhJindal Jan 5, 2026
9ca9e3b
minor changes as per review
AnirudhJindal Jan 8, 2026
c7799ef
Remove accidentally committed node_modules
AnirudhJindal Jan 11, 2026
343d4ef
added a github action for automating addition of test-ids
AnirudhJindal Mar 3, 2026
ac90ab4
changed the script restore method to the path filtring
AnirudhJindal Mar 6, 2026
ef1c568
Fix some remote categorization for easier dialect-specific loading
jdesrosiers Mar 10, 2026
9cb5e89
All dependencies should be devDependencies
jdesrosiers Mar 10, 2026
61f1940
Fix issues with v1 tests
jdesrosiers Mar 10, 2026
ed50aef
Add alternate version of script and action
jdesrosiers Mar 10, 2026
4d98634
Disable generating test ids for now
jdesrosiers Mar 10, 2026
470010d
Try to fix action checkout bug
jdesrosiers Mar 10, 2026
46f5725
accept upstream changes and simplyfying apply-test-ids.yml
AnirudhJindal Mar 14, 2026
323a738
fix: accept incoming changes for package files
AnirudhJindal Mar 14, 2026
db70c2c
removed the not needed second test file filter
AnirudhJindal Mar 14, 2026
bbb9c24
changed the node version to latest
AnirudhJindal Mar 14, 2026
d14cdd4
fix: updated the script
AnirudhJindal Mar 19, 2026
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
11 changes: 10 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
{
"name": "json-schema-test-suite",
"version": "0.1.0",
"type": "module",
"description": "A language agnostic test suite for the JSON Schema specifications",
"repository": "github:json-schema-org/JSON-Schema-Test-Suite",
"keywords": [
"json-schema",
"tests"
],
"author": "http://json-schema.org",
"license": "MIT"
"license": "MIT",
"dependencies": {
"@hyperjump/browser": "^1.3.1",
"@hyperjump/json-pointer": "^1.1.1",
"@hyperjump/json-schema": "^1.17.2",
"@hyperjump/pact": "^1.4.0",
"@hyperjump/uri": "^1.3.2",
"json-stringify-deterministic": "^1.0.12"
}
}
100 changes: 100 additions & 0 deletions scripts/add-test-ids.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import * as fs from "node:fs";
import * as crypto from "node:crypto";
import jsonStringify from "json-stringify-deterministic";
import { parse, modify, applyEdits } from "jsonc-parser";
import { normalize } from "./normalize.js";
import { loadRemotes } from "./load-remotes.js";


const DIALECT_MAP = {
"draft2020-12": "https://json-schema.org/draft/2020-12/schema",
"draft2019-09": "https://json-schema.org/draft/2019-09/schema",
"draft7": "http://json-schema.org/draft-07/schema#",
"draft6": "http://json-schema.org/draft-06/schema#",
"draft4": "http://json-schema.org/draft-04/schema#"
};


function generateTestId(normalizedSchema, testData, testValid) {
return crypto
.createHash("md5")
.update(
jsonStringify(normalizedSchema) +
jsonStringify(testData) +
testValid
)
.digest("hex");
}

async function addIdsToFile(filePath, dialectUri) {
console.log("Reading:", filePath);

const text = fs.readFileSync(filePath, "utf8");
const tests = parse(text);
let edits = [];
let added = 0;

for (let i = 0; i < tests.length; i++) {
const testCase = tests[i];
const normalizedSchema = await normalize(testCase.schema, dialectUri);

for (let j = 0; j < testCase.tests.length; j++) {
const test = testCase.tests[j];

if (!test.id) {
const id = generateTestId(
normalizedSchema,
test.data,
test.valid
);

const path = [i, "tests", j, "id"];

edits.push(
...modify(text, path, id, {
formattingOptions: {
insertSpaces: true,
tabSize: 2
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The convention for tabSize in this repo is 4, not 2.

}
})
);

added++;
}
}
}

if (added > 0) {
const updatedText = applyEdits(text, edits);
fs.writeFileSync(filePath, updatedText);
console.log(` Added ${added} IDs`);
} else {
console.log(" All tests already have IDs");
}
}

//CLI stuff

const dialectArg = process.argv[2];
if (!dialectArg || !DIALECT_MAP[dialectArg]) {
console.error("Usage: node add-test-ids.js <dialect> [file-path]");
console.error("Available dialects:", Object.keys(DIALECT_MAP).join(", "));
process.exit(1);
}

const dialectUri = DIALECT_MAP[dialectArg];
const filePath = process.argv[3];

// Load remotes only for the specified dialect
loadRemotes(dialectUri, "./remotes");

if (filePath) {
await addIdsToFile(filePath, dialectUri);
} else {
const testDir = `tests/${dialectArg}`;
const files = fs.readdirSync(testDir).filter(f => f.endsWith(".json"));

for (const file of files) {
await addIdsToFile(`${testDir}/${file}`, dialectUri);
}
}
140 changes: 140 additions & 0 deletions scripts/check-test-ids.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import * as fs from "node:fs";
import * as path from "node:path";
import * as crypto from "node:crypto";
import jsonStringify from "json-stringify-deterministic";
import { normalize } from "./normalize.js";
import { loadRemotes } from "./load-remotes.js";


// Helpers

function* jsonFiles(dir) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
yield* jsonFiles(full);
} else if (entry.isFile() && entry.name.endsWith(".json")) {
yield full;
}
}
}

function dialectFromDir(dir) {
const draft = path.basename(dir);

switch (draft) {
case "draft2020-12":
return "https://json-schema.org/draft/2020-12/schema";
case "draft2019-09":
return "https://json-schema.org/draft/2019-09/schema";
case "draft7":
return "http://json-schema.org/draft-07/schema#";
case "draft6":
return "http://json-schema.org/draft-06/schema#";
case "draft4":
return "http://json-schema.org/draft-04/schema#";
default:
throw new Error(`Unknown draft directory: ${draft}`);
}
}

function generateTestId(normalizedSchema, testData, testValid) {
return crypto
.createHash("md5")
.update(
jsonStringify(normalizedSchema) +
jsonStringify(testData) +
testValid
)
.digest("hex");
}



async function checkVersion(dir) {
const missingIdFiles = new Set();
const mismatchedIdFiles = new Set();

const dialectUri = dialectFromDir(dir);

console.log(`Checking tests in ${dir}...`);
console.log(`Using dialect: ${dialectUri}`);

// Load remotes ONCE for this dialect
const remotesPath = "./remotes";
if (fs.existsSync(remotesPath)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The remotes directory always exists. You don't have to check for it.

loadRemotes(dialectUri, remotesPath);
}

for (const file of jsonFiles(dir)) {
const testCases = JSON.parse(fs.readFileSync(file, "utf8"));

for (const testCase of testCases) {
const normalizedSchema = await normalize(testCase.schema, dialectUri);

for (const test of testCase.tests) {
if (!test.id) {
missingIdFiles.add(file);
console.log(
` ✗ Missing ID: ${file} | ${testCase.description} | ${test.description}`
);
continue;
}

const expectedId = generateTestId(
normalizedSchema,
test.data,
test.valid
);

if (test.id !== expectedId) {
mismatchedIdFiles.add(file);
console.log(` ✗ Mismatched ID: ${file}`);
console.log(
` Test: ${testCase.description} | ${test.description}`
);
console.log(` Current ID: ${test.id}`);
console.log(` Expected ID: ${expectedId}`);
}
}
}
}

//Summary
console.log("\n" + "=".repeat(60));
console.log("Summary:");
console.log("=".repeat(60));

console.log("\nFiles with missing IDs:");
missingIdFiles.size === 0
? console.log(" ✓ None")
: [...missingIdFiles].forEach(f => console.log(` - ${f}`));

console.log("\nFiles with mismatched IDs:");
mismatchedIdFiles.size === 0
? console.log(" ✓ None")
: [...mismatchedIdFiles].forEach(f => console.log(` - ${f}`));

const hasErrors =
missingIdFiles.size > 0 || mismatchedIdFiles.size > 0;

console.log("\n" + "=".repeat(60));
if (hasErrors) {
console.log("❌ Check failed - issues found");
process.exit(1);
} else {
console.log("✅ All checks passed!");
}
}


// CLI


const dir = process.argv[2];
if (!dir) {
console.error("Usage: node scripts/check-test-ids.js <tests/draftXXXX>");
process.exit(1);
}

await checkVersion(dir);
45 changes: 45 additions & 0 deletions scripts/load-remotes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// scripts/load-remotes.js
import * as fs from "node:fs";
import { toAbsoluteIri } from "@hyperjump/uri";
import { registerSchema } from "@hyperjump/json-schema/draft-2020-12";

// Keep track of which remote URLs we've already registered
const loadedRemotes = new Set();

export const loadRemotes = (dialectId, filePath, url = "") => {
if (!fs.existsSync(filePath)) {
console.warn(`Warning: Remotes path not found: ${filePath}`);
return;
}

fs.readdirSync(filePath, { withFileTypes: true }).forEach((entry) => {
if (entry.isFile() && entry.name.endsWith(".json")) {
const remotePath = `${filePath}/${entry.name}`;
const remoteUrl = `http://localhost:1234${url}/${entry.name}`;

// Skip if already registered
if (loadedRemotes.has(remoteUrl)) {
return;
}

const remote = JSON.parse(fs.readFileSync(remotePath, "utf8"));

// FIXEDhere
if (typeof remote.$id === "string" && remote.$id.startsWith("file:")) {
remote.$id = remote.$id.replace(/^file:/, "x-file:");
}

// Only register if $schema matches dialect OR there's no $schema
if (!remote.$schema || toAbsoluteIri(remote.$schema) === dialectId) {
registerSchema(remote, remoteUrl, dialectId);
loadedRemotes.add(remoteUrl);
}
} else if (entry.isDirectory()) {
loadRemotes(
dialectId,
`${filePath}/${entry.name}`,
`${url}/${entry.name}`
);
}
});
};
Loading
Loading