import { useMemo, useState } from "react"; import { triggerBlobDownload } from "@add-ideas/toolbox-helpers"; import { SCHEMA_LIMITS, compareSchemas, generateSample, inspectWorkspace, validateJsonInstance, type Diagnostic, type SampleResult, type SchemaChange, type SchemaLanguage, type SchemaWorkspace, type WorkspaceInput, } from "../schema/model"; const mainSchema = `{ "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Person", "type": "object", "required": ["name", "address"], "properties": { "name": { "type": "string", "minLength": 1 }, "age": { "type": "integer", "minimum": 0 }, "address": { "$ref": "address.schema.json#/$defs/address" } }, "additionalProperties": false }`; const addressSchema = `{ "$schema": "https://json-schema.org/draft/2020-12/schema", "$defs": { "address": { "type": "object", "required": ["city"], "properties": { "city": { "type": "string" }, "postalCode": { "type": "string" } } } } }`; const sampleDocuments: WorkspaceInput[] = [ { name: "person.schema.json", source: mainSchema, language: "json-schema" }, { name: "address.schema.json", source: addressSchema, language: "json-schema", }, ]; const sampleInstance = `{ "name": "Ada Lovelace", "age": 36, "address": { "city": "London", "postalCode": "SW1A" } }`; const beforeSchema = `{ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "id": { "type": "string" }, "label": { "type": "string" } }, "required": ["id"] }`; const afterSchema = `{ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "id": { "type": "string" }, "label": { "type": "string" }, "kind": { "type": "string" } }, "required": ["id", "kind"], "additionalProperties": false }`; type Tab = "workspace" | "validate" | "sample" | "references" | "compare"; const languageNames: Record = { "json-schema": "JSON Schema", openapi: "OpenAPI", xsd: "XSD", "relax-ng": "Relax NG", schematron: "Schematron", }; const tabs: Array<{ id: Tab; label: string }> = [ { id: "workspace", label: "Workspace" }, { id: "validate", label: "Validate" }, { id: "sample", label: "Samples" }, { id: "references", label: "References" }, { id: "compare", label: "Compare" }, ]; function message(reason: unknown, fallback: string): string { return reason instanceof Error ? reason.message : fallback; } function download(value: string, filename: string, type: string) { triggerBlobDownload(new Blob([value], { type }), filename); } function DiagnosticList({ diagnostics, }: { diagnostics: readonly Diagnostic[]; }) { if (!diagnostics.length) return

No diagnostics.

; return ( ); } export function Workbench() { const initial = useMemo( () => inspectWorkspace(sampleDocuments, "person.schema.json"), [], ); const [tab, setTab] = useState("workspace"); const [documents, setDocuments] = useState(sampleDocuments); const [selected, setSelected] = useState(0); const [report, setReport] = useState(initial); const [error, setError] = useState(""); const [instance, setInstance] = useState(sampleInstance); const [validation, setValidation] = useState([]); const [sample, setSample] = useState(); const [before, setBefore] = useState(beforeSchema); const [after, setAfter] = useState(afterSchema); const [comparison, setComparison] = useState([]); const current = documents[selected]; const reportEntry = report.documents.get(report.entry); const updateCurrent = (patch: Partial) => { setDocuments((items) => items.map((item, index) => index === selected ? { ...item, ...patch } : item, ), ); }; const inspect = () => { try { const next = inspectWorkspace(documents, current?.name); setReport(next); setValidation([]); setSample(undefined); setError(""); } catch (reason) { setError(message(reason, "Could not inspect this workspace.")); } }; const openFiles = async (files: FileList | null) => { if (!files?.length) return; try { if (files.length > SCHEMA_LIMITS.documents) throw new RangeError( `Select at most ${SCHEMA_LIMITS.documents} files.`, ); const selectedFiles = Array.from(files); if ( selectedFiles.some( (file) => file.size > SCHEMA_LIMITS.documentChars * 4, ) ) throw new RangeError( "A schema file exceeds the conservative 8 MiB UTF-8 byte gate.", ); if ( selectedFiles.reduce((sum, file) => sum + file.size, 0) > SCHEMA_LIMITS.workspaceChars * 4 ) throw new RangeError( "Selected files exceed the conservative 32 MiB byte gate.", ); const next = await Promise.all( selectedFiles.map(async (file) => ({ name: file.webkitRelativePath || file.name, source: await file.text(), language: "auto" as const, })), ); const inspected = inspectWorkspace(next, next[0]?.name); setDocuments(next); setSelected(0); setReport(inspected); setValidation([]); setSample(undefined); setError(""); } catch (reason) { setError(message(reason, "Could not open the selected schema files.")); } }; const addDocument = () => { if (documents.length >= SCHEMA_LIMITS.documents) { setError(`Workspace already has ${SCHEMA_LIMITS.documents} documents.`); return; } const next = [ ...documents, { name: `schema-${documents.length + 1}.json`, source: "{}\n", language: "auto" as const, }, ]; setDocuments(next); setSelected(next.length - 1); }; const removeDocument = () => { if (documents.length === 1) { setError("A workspace needs at least one document."); return; } const next = documents.filter((_, index) => index !== selected); setDocuments(next); setSelected(Math.min(selected, next.length - 1)); }; const validate = () => { try { const result = validateJsonInstance(report, instance); setValidation(result.diagnostics); setError(""); } catch (reason) { setError(message(reason, "Could not validate the instance.")); } }; const createSample = () => { try { setSample(generateSample(report)); setError(""); } catch (reason) { setError(message(reason, "Could not generate a bounded sample.")); } }; const compare = () => { try { setComparison( compareSchemas( { name: "before.schema.json", source: before, language: "auto" }, { name: "after.schema.json", source: after, language: "auto" }, ), ); setError(""); } catch (reason) { setError(message(reason, "Could not compare these schemas.")); } }; return (

Schema workbench

Schema Tools

Inspect local schema workspaces, trace references, validate bounded JSON instances, derive samples, and review conservative compatibility signals.

Browser-local

Honest capability tiers

Five languages, deliberately different guarantees

JSON Schema focused validation

CSP-safe Draft 6/7, 2019-09, and 2020-12 assertion subset, with bounded local references. Regex-bearing keywords are not executed.

OpenAPI 3 focused checks

JSON/YAML parsing, operations, references, examples, and conservative breaking-change signals—not full conformance.

XSD structural only

Well-formed XML, declarations/imports, and heuristic instance samples. No XSD instance validation.

Relax NG structural only

XML-syntax grammar inventory, local includes, and bounded first-branch samples. Compact syntax is not supported.

Schematron inert inspection

Patterns, rules, assertions, query binding, and includes are inventoried. XPath and extensions are never executed.

{tabs.map((item) => ( ))}
{error && (

{error} The last successful report remains available.

)} {tab === "workspace" && (
{documents.map((item, index) => ( ))}
{current && (