Schema and JSON Loading
What It Is
Schema loading is the guardrail between untrusted JSON and the dispatcher registry. A DAG can arrive from a plugin package, config file, database row, or generated artifact; DAGDocument.load accepts it only after the JSON parses and the document satisfies DAGSchema.
The schema is the same contract the builder emits. That keeps code-authored DAGs, serialized JSON-LD DAGs, docs diagrams, and runtime execution on one shape.
How It Works
DAGDocument.load parses raw JSON, validates it against DAGSchema, and returns a typed DAG only after every placement satisfies its schema. Validator exposes the same precompiled Ajv validators for lower-level entity checks.
DAGSchema describes the canonical DAG wire shape in JSON Schema 2020-12. The Ajv 2020-12 instance that validates against it is compiled once at module load and exposed through Validator.dag. Application code calls Validator.dag.validate(x); it does not need to build a fresh Ajv instance against the package schemas.
Diagrams, Examples, and Outputs
Example 03 starts with a JSON-LD string, validates it, registers the loaded DAG, and runs it. The JSON-LD and diagram are generated from that same example source:
const dagJson = JSON.stringify({
'@context': DAG_CONTEXT,
'@id': 'urn:noocodec:dag:from-json',
'@type': 'DAG',
'name': 'from-json',
'version': '1',
'entrypoints': { 'main': 'urn:noocodec:dag:from-json/node/echo' },
'nodes': [
{
'@id': 'urn:noocodec:dag:from-json/node/echo',
'@type': 'SingleNode',
'name': 'echo',
'node': 'urn:noocodec:node:echo',
'outputs': { 'success': 'urn:noocodec:dag:from-json/node/end' },
},
{
'@id': 'urn:noocodec:dag:from-json/node/end',
'@type': 'TerminalNode',
'name': 'end',
'outcome': 'completed',
},
],
});// DAGDocument.load() throws DAGError (code VALIDATION_ERROR) if JSON is malformed or schema fails.
export const dag = DAGDocument.load(dagJson);Example 03 schema-loaded DAG
2 placements{
"@context": {
"@version": 1.1,
"name": {
"@id": "https://noocodec.dev/ontology/dag/name"
},
"version": {
"@id": "https://noocodec.dev/ontology/dag/version"
},
"entrypoints": {
"@id": "https://noocodec.dev/ontology/dag/entrypoints",
"@container": "@index"
},
"nodes": {
"@id": "https://noocodec.dev/ontology/dag/nodes",
"@container": "@set"
},
"outputs": {
"@id": "https://noocodec.dev/ontology/dag/outputs"
},
"node": {
"@id": "https://noocodec.dev/ontology/dag/node"
},
"dag": {
"@id": "https://noocodec.dev/ontology/dag/dag"
},
"body": {
"@id": "https://noocodec.dev/ontology/dag/body"
},
"source": {
"@id": "https://noocodec.dev/ontology/dag/source"
},
"sources": {
"@id": "https://noocodec.dev/ontology/dag/sources",
"@container": "@index"
},
"itemKey": {
"@id": "https://noocodec.dev/ontology/dag/itemKey"
},
"execution": {
"@id": "https://noocodec.dev/ontology/dag/execution"
},
"concurrency": {
"@id": "https://noocodec.dev/ontology/dag/concurrency"
},
"throttle": {
"@id": "https://noocodec.dev/ontology/dag/throttle"
},
"reservoir": {
"@id": "https://noocodec.dev/ontology/dag/reservoir"
},
"gather": {
"@id": "https://noocodec.dev/ontology/dag/gather"
},
"dagReference": {
"@id": "https://noocodec.dev/ontology/dag/dagReference",
"@type": "@id"
},
"DagReference": {
"@id": "https://noocodec.dev/ontology/dag/DagReference"
},
"from": {
"@id": "https://noocodec.dev/ontology/dag/from"
},
"path": {
"@id": "https://noocodec.dev/ontology/dag/path"
},
"candidates": {
"@id": "https://noocodec.dev/ontology/dag/candidates",
"@container": "@set"
},
"candidateDag": {
"@id": "https://noocodec.dev/ontology/dag/candidateDag",
"@type": "@id"
},
"selectedDag": {
"@id": "https://noocodec.dev/ontology/dag/selectedDag",
"@type": "@id"
},
"resultField": {
"@id": "https://noocodec.dev/ontology/dag/resultField"
},
"policy": {
"@id": "https://noocodec.dev/ontology/dag/policy"
},
"reducer": {
"@id": "https://noocodec.dev/ontology/dag/reducer"
},
"outcome": {
"@id": "https://noocodec.dev/ontology/dag/outcome"
},
"phase": {
"@id": "https://noocodec.dev/ontology/dag/phase"
},
"stateMapping": {
"@id": "https://noocodec.dev/ontology/dag/stateMapping"
},
"container": {
"@id": "https://noocodec.dev/ontology/dag/container"
},
"DAG": {
"@id": "https://noocodec.dev/ontology/dag/DAG"
},
"Placement": {
"@id": "https://noocodec.dev/ontology/dag/Placement"
},
"SingleNode": {
"@id": "https://noocodec.dev/ontology/dag/SingleNode"
},
"ScatterNode": {
"@id": "https://noocodec.dev/ontology/dag/ScatterNode"
},
"EmbeddedDAGNode": {
"@id": "https://noocodec.dev/ontology/dag/EmbeddedDAGNode"
},
"GatherNode": {
"@id": "https://noocodec.dev/ontology/dag/GatherNode"
},
"TerminalNode": {
"@id": "https://noocodec.dev/ontology/dag/TerminalNode"
},
"PhaseNode": {
"@id": "https://noocodec.dev/ontology/dag/PhaseNode"
}
},
"@id": "urn:noocodec:dag:from-json",
"@type": "DAG",
"name": "from-json",
"version": "1",
"entrypoints": {
"main": "urn:noocodec:dag:from-json/node/echo"
},
"nodes": [
{
"@id": "urn:noocodec:dag:from-json/node/echo",
"@type": "SingleNode",
"name": "echo",
"node": "urn:noocodec:node:echo",
"outputs": {
"success": "urn:noocodec:dag:from-json/node/end"
}
},
{
"@id": "urn:noocodec:dag:from-json/node/end",
"@type": "TerminalNode",
"name": "end",
"outcome": "completed"
}
]
}Mermaid source
%%{init: {"flowchart":{"nodeSpacing":92,"rankSpacing":104,"padding":28}}}%%
flowchart TB
%% from-json (v1)
entry_main(["main"])
entry_main --> urn_noocodec_dag_from-json/node/echo
urn_noocodec_dag_from-json/node/echo["echo"]
urn_noocodec_dag_from-json/node/echo -->|success| urn_noocodec_dag_from-json/node/end
urn_noocodec_dag_from-json/node/end((("end")))Use these pages together:
- Example 03: Tool Schemas runs the load, validation, and round-trip path.
- JSON-LD Export and Import explains the serialized wire format.
- DAGBuilder explains the code path that emits the same schema-valid DAG shape.
- Reference: Entities lists every schema-derived entity type.
What It Lets You Do
Use when
Use schema loading when a DAG document comes from outside trusted TypeScript source: a plugin package, config file, database row, user upload, or generated artifact. Validation is the boundary that keeps malformed JSON-LD out of the dispatcher registry.
Code Samples
DAGSchema
The schema is exported directly for callers that want to integrate with their own schema registry:
process.stdout.write(`DAGSchema.$id: ${DAGSchema.$id}\n`);
// → 'https://noocodec.dev/schemas/dagonizer/DAG'The schema covers @id, name, version, entrypoints, and nodes. Each placement variant has its own sub-schema enforcing required fields and valid enumerations for @type, gather strategy, and output target values. Schema validation checks that the document has the right wire shape; semantic validation then verifies that referenced DAG IRIs, node IRIs, gather sources, and placement route targets resolve.
@type | Required fields | Notes |
|---|---|---|
SingleNode | @id, @type, name, node, outputs | outputs route to placement IRIs |
ScatterNode | @id, @type, name, body, source, outputs | body is { node } or { dag }; DAG bodies use literal DAG IRIs/CURIEs or DagReference; optional itemKey, execution (unified concurrency-limiting policy), stateMapping.input, reducer |
EmbeddedDAGNode | @id, @type, name, dag, outputs | dag is the registered child DAG IRI/CURIE or DagReference; optional stateMapping (input and output field maps) |
GatherNode | @id, @type, name, sources, gather, outputs | first-class fan-in barrier; sources and outputs point at placement IRIs; optional policy controls all, any, or quorum readiness |
TerminalNode | @id, @type, name, outcome | no outputs field; outcome is 'completed' or 'failed' |
PhaseNode | @id, @type, name, phase, node | phase is 'pre' or 'post'; no outputs |
Details for Nerds
DAGDocument.load
The single permitted entry point for raw external JSON:
// DAGDocument.load() is the single ingest boundary: parse JSON, validate schema,
// return a fully-typed DAG or throw DAGError (code VALIDATION_ERROR).
const dispatcher = new Dagonizer<NodeStateBase>();
dispatcher.registerNode(new EchoNode());
dispatcher.registerDAG(dag);
const state = new NodeStateBase();
await dispatcher.execute('urn:noocodec:dag:from-json', state);DAGDocument.load calls JSON.parse then validates the result against DAGSchema. Both JSON syntax errors and schema violations throw ValidationError with a human-readable message listing every failing constraint.
Example 03 exercises the validation path with a deliberately broken document:
try {
// Missing '@context', '@id', '@type', 'entrypoints', 'nodes': schema rejects it.
DAGDocument.load(JSON.stringify({ "name": 'broken', "version": '1' }));
} catch (error) {
if (error instanceof DAGError && error.code === 'VALIDATION_ERROR') {
const firstLine = error.message.split('\n')[0];
process.stdout.write(`DAGError (first failure): ${firstLine}\n`);
}
}Validator.dag
The lower-level schema validator used by DAGDocument.load and semantic registration checks:
// Validator.dag.validate() returns a narrowed DAG or throws DAGError (code VALIDATION_ERROR).
// This is the lower-level call that DAGDocument.load and registerDAG use.
const validated = Validator.dag.validate(JSON.parse(DAGDocument.serialize(dag)));
process.stdout.write(`Validator.dag.validate @type: ${validated['@type']}\n`);registerDAG calls Validator.dag.validate as a pre-pass before the semantic checks (node and DAG cross-references).
DAGDocument.serialize
Round-trip a validated DAG to JSON:
// DAGDocument.serialize(dag) produces pretty-printed JSON.
// DAGDocument.load(json) parses and validates it back to a typed DAG.
// The result is structurally identical to the original.
const serialized = DAGDocument.serialize(dag);
const roundTripped = DAGDocument.load(serialized);
const isEqual = JSON.stringify(roundTripped) === JSON.stringify(dag);
process.stdout.write(`Round-trip equal: ${String(isEqual)}\n`);DAGDocument.serialize is JSON.stringify(dag, null, 2). It does not re-validate; the DAG is assumed to already be valid.
DAGDocument.serialize produces JSON for persistence or transport. Use JSON.stringify only when the example is showing generic JSON formatting rather than the framework API.
ValidationError
ValidationError extends DAGError and is thrown for schema violations:
// DAGError exposes a machine-readable code and a human-readable message.
try {
DAGDocument.load('{ "name": "broken" }');
} catch (error) {
if (error instanceof DAGError && error.code === 'VALIDATION_ERROR') {
process.stdout.write(`code: ${error.code}\n`); // 'VALIDATION_ERROR'
process.stdout.write(`message: ${error.message.split('\n')[0]}\n`);
}
}Each Ajv failure is formatted as <instancePath>: <message> on a separate line.
Validator sub-validators
Validator exposes one EntityValidator<T> per entity schema. Each sub-validator has three methods:
// Each sub-validator exposes three methods:
const unknownValue: unknown = JSON.parse(DAGDocument.serialize(dag));
const isValid = Validator.dag.is(unknownValue); // type predicate → boolean
const dagAgain = Validator.dag.validate(unknownValue); // returns DAG or throws
const errs = Validator.dag.errors(unknownValue); // string[] | null (null = valid)
process.stdout.write(`is: ${String(isValid)}, errors: ${String(errs)}, name: ${dagAgain.name}\n`);Sub-validators are compiled once at module load against the shared Ajv 2020-12 instance (allErrors: true, strict: false). Every top-level entity schema in entities/ has a corresponding sub-validator on Validator, including Validator.terminalNode for TerminalNodeSchema:
// Every top-level entity schema has a corresponding sub-validator on Validator.
const terminalValue: unknown = dag.nodes[1]; // TerminalNode placement from dag-literal
const isTerminal = Validator.terminalNode.is(terminalValue);
process.stdout.write(`is TerminalNode: ${String(isTerminal)}\n`);Re-validating a value calls the precompiled function. There is no Ajv setup cost per call.
Related Concepts
- DAGBuilder - author DAGs in code instead of loading from JSON
- JSON-LD export and import - serialize, load, and round-trip a DAG document
- Entities - every schema and its derived type
- Example 03: Tool Schemas - runnable load-and-validate example
- Reference, Validation
- Reference, Entities
- Reference, Errors,
ValidationError