Skip to content

YAML + Zod

YAML + Zod ↗ makes a neat pair for configurations or DSLs.

When Zod validation fails, it gives back the path to the offending value (e.g. ["resources", 0, "type"]). The yaml ↗ package can parse YAML into an AST that retains source positions. Combining the two, you can point the user to the exact line and column in their YAML file:

import {
LineCounter,
Node,
ParsedNode,
parseDocument,
Scalar,
YAMLMap,
YAMLSeq,
parse as parseYaml,
} from "yaml";
import { ZodError } from "zod";
type Path = (string | number)[];
function traverseAst(
ast: ParsedNode | null | Scalar<ParsedNode | null>,
path: Path
): null | Node {
if (path.length === 0 || ast === null) return ast;
const [current, ...rest] = path;
if (ast instanceof YAMLMap) {
return traverseAst(ast.get(current, true) ?? null, rest);
}
if (ast instanceof YAMLSeq) {
return traverseAst(ast.get(current, true) ?? null, rest);
}
throw new Error(`Unexpected node type: ${ast.constructor}`);
}
function getLineCol(file: string, path: Path) {
const lineCounter = new LineCounter();
const ast = parseDocument(file, {
keepSourceTokens: true,
lineCounter,
});
const srcToken = traverseAst(ast.contents, path)?.srcToken;
if (srcToken) return lineCounter.linePos(srcToken.offset);
}
function constructError(
message: string,
path: Path,
file?: string
) {
const pos = file ? getLineCol(file, path) : undefined;
return new Error(
`${message} ${pos ? `at ${pos.line}:${pos.col} ` : ""}(${path.join("/")})`
);
}

Then in the parse function, catch ZodError and map it to a user-friendly message:

import { mySchema } from "./schema.js";
function parse(file: string) {
const rawData = parseYaml(file);
try {
return mySchema.parse(rawData);
} catch (e) {
if (e instanceof ZodError) {
const path = e.errors[0].path;
throw constructError(e.errors[0].message, path, file);
}
throw e;
}
}

This gives errors like: Wrong value "ec2" at 3:12 (resources/0/type) instead of a generic Zod message.

Zod schemas can be converted to JSON Schema with zod-to-json-schema ↗. Once you have a JSON Schema, the YAML extension for VS Code ↗ provides autocomplete and validation in the editor.

You can also use an inlined schema ↗ comment at the top of the YAML file:

# yaml-language-server: $schema=https://example.com/schema.json

So the workflow is:

  1. Define your schema with Zod (single source of truth)
  2. Use it at runtime for validation with precise error locations
  3. Convert to JSON Schema for IDE support

diagrams-as-code ↗ uses this approach. YAML serves as the DSL for describing infrastructure diagrams, Zod validates the structure, and errors point to exact positions in the source file.