Markdown I/O
gen_markdown_io generates the typed boundary between your schema
entities and their on-disk YAML-frontmatter form: one {Entity}Frontmatter
module per entity, built on the
markdown-store
runtime crate. It also returns the MarkdownIoOutput metadata that
gen_store consumes when the store backend is
Backend::Markdown — see the markdown backend
guide for the full store story; this page covers
the persistence layer itself.
Configuration
Section titled “Configuration”let md = ontogen::gen_markdown_io(&schema.entities, &ontogen::MarkdownIoConfig { output_dir: "src/persistence/markdown/generated".into(), vault_root: "data/vault".into(), // where the .md files live layout: ontogen::MarkdownLayout::PerEntityDir, // data/vault/<entity>/<id>.md id_strategy: ontogen::IdStrategy::SlugFromField("title".into()), list_cap: 10_000, // loud error past this many records})?;The vault fields describe the store’s runtime shape and flow into the
returned MarkdownIoOutput; the generated files land in output_dir.
What gets generated
Section titled “What gets generated”For each entity, a module like:
/// Frontmatter view of a Task: every persisted field except the id (the/// filename stem), the markdown body, and derived `has_many` views.#[derive(Debug, Clone, Serialize, Deserialize)]pub struct TaskFrontmatter { pub title: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub epic_id: Option<String>, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub tags: Vec<String>,}
pub const TASK_FM_FIELDS: &[&str] = &["title", "epic_id", "tags"];
impl TaskFrontmatter { pub fn from_task(value: &Task) -> Self { /* wikilink-encodes relations */ } pub fn into_task(self, id: String, body: String) -> Task { /* strips them */ }}Three rules decide what’s in the struct:
- the id is never frontmatter — it’s the filename stem;
- the body is never frontmatter — it’s the markdown after the fence
(
#[ontology(body)]threads it throughinto_*’s signature); has_manyfields are never frontmatter — they’re derived views, reconstructed by walking the child folder (storing them would create dual-write drift against the child foreign keys).belongs_toandmany_to_manyfields ARE stored, as wikilinks.
{ENTITY}_FM_FIELDS is the owned-key set passed to the runtime’s
Document::merge_serialize: cleared options remove their keys, and any keys
a human added by hand survive every generated rewrite.
The wikilink boundary
Section titled “The wikilink boundary”from_* / into_* are the only place wikilink syntax exists:
on disk in your codeepic_id: '[[E0042]]' ⇄ task.epic_id == Some("E0042")tags: ⇄ task.tags == vec!["codegen"]- '[[codegen]]'The same files render as a navigable graph in Obsidian; your API emits clean ids. Round-trips are lossless in both directions, and untouched files re-render byte-for-byte.
Standalone use
Section titled “Standalone use”You don’t need the store layer to use this boundary — parse and write
records directly against markdown-store:
let doc = vault.read_record("tasks", "ship-the-emitter")?;let fm: TaskFrontmatter = doc.deserialize()?;let task = fm.into_task("ship-the-emitter".into(), doc.body().to_string());(The four vault config fields are still required in MarkdownIoConfig even
for leaf-only use — the deliberate cost of the backend being a first-class
citizen rather than a side door.)