Use Markdown as a Data Format

Keep your content human-editable, but make it machine-readable.
Use Markdown as a data format for your apps, APIs, and websites.
Parse text into typed, validated data using a robust AST.

import { datamark } from "datamark"
const Blog = datamark({ schema, parse, stringify });
const markdown = `# Hello World
This is a blog post with a [link](...).
## A Section
More content [here](...)...`
let data = Blog.parse(markdown);
// {
// title: "Hello World",
// snippet: "This is a blog post with a link.",
// sections: [{ heading: "A Section", content: "More content here..." }]
// links: [{ href: "...", text: "link" }, { href: "...", text: "here" }]
// }
data.title = "Hello from datamark!"
const out = Blog.stringify(data);
// → # Hello from datamark!\n\nThis is a blog post with...

Parse

datamark parses Markdown into a typed AST with sections, paragraphs, code blocks, lists, and tables. The result is a Document you can traverse, query, and transform.
import { parse } from "datamark"
const doc = parse("# Hello");
doc.root.children[0].heading.children
// → [{ type: "text", value: "Hello" }]

Format

A format is a typed contract. Define a schema, a parse function that walks the AST, and a stringify function for round-trip serialization. Add inline examples and your format becomes self-testing.
import { datamark } from "datamark"
import { heading, paragraph } from "datamark/stringify"
import z from "zod"
const Blog = datamark({
schema: z.object({
title: z.string(),
body: z.string(),
}),
parse(doc) {
return {
title: doc.root.children[0].heading.children[0].value,
body: doc.root.children[1].paragraph.children[0].value
};
},
stringify(data) {
return [
heading(data.title),
paragraph(data.body),
].join("\\n\\n");
}
})

Extract

The AST SDK gives you primitives to query the tree: findAll for filtering, textContent for extracting text, extractTodoItems for checkbox lists. Navigate sections by heading, split by depth, or flatten back to blocks.
import { findAll, isCodeBlock, textContent } from "datamark"
// Find all TypeScript code blocks
const blocks = findAll(
doc.root,
n => isCodeBlock(n, "typescript"),
);
// Extract all text from a section
const body = textContent(doc.root).trim();

Validate

Bring your own validator. datamark speaks Standard Schema v1, so Zod, Valibot, ArkType, and TypeBox all work out of the box. Schemas are validated automatically — bad data throws ValidationError with a clear message, not an undefined panic.
import v from "valibot"
const Recipe = datamark({
frontmatterSchema: v.object({
prepTime: v.string(),
servings: v.number(),
}),
schema: v.object({
title: v.string(),
ingredients: v.array(v.string()),
}),
parse(doc) {
// frontmatter is typed — no `as any`
const { prepTime, servings } = doc.frontmatter
...
},
})

Why datamark?

Fully type-safe

Schemas drive the types. Your parse function receives typed frontmatter and returns a typed object. No manual type definitions, no as any escapes.

Self-documenting formats

A format is a schema + parse + stringify + examples. The inline examples are validated by .test() automatically — your documentation stays correct.

Round-trip fidelity

Parse Markdown into typed objects, then stringify them back. The builder primitives handle escaping and indentation so you never write brittle string templates again.

Get started in 30 seconds.

npm install datamark