Skip to content

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:

ts
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',
    },
  ],
});
ts
// 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
DAG JSON-LD registered with the dispatcher
{
  "@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 generated from the same DAG
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:

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:

ts
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.

@typeRequired fieldsNotes
SingleNode@id, @type, name, node, outputsoutputs route to placement IRIs
ScatterNode@id, @type, name, body, source, outputsbody 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, outputsdag is the registered child DAG IRI/CURIE or DagReference; optional stateMapping (input and output field maps)
GatherNode@id, @type, name, sources, gather, outputsfirst-class fan-in barrier; sources and outputs point at placement IRIs; optional policy controls all, any, or quorum readiness
TerminalNode@id, @type, name, outcomeno outputs field; outcome is 'completed' or 'failed'
PhaseNode@id, @type, name, phase, nodephase is 'pre' or 'post'; no outputs

Details for Nerds

DAGDocument.load

The single permitted entry point for raw external JSON:

ts
// 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:

ts
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:

ts
// 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:

ts
// 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:

ts
// 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:

ts
// 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:

ts
// 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.

Watched over by the Order of Dagon.