json2md is a Rust library that validates JSON and renders it as Markdown.
Keep a document's content separate from its layout, and check its fields and
values before producing the document.
This example creates a release note with a title, summary, and list of changes.
The JSON input holds the note's content:
{
"title": "Release 0.1",
"summary": "JSON data becomes a checked Markdown document.",
"changes": [
"Validate input before rendering.",
"Reuse compiled schemas and templates."
]
}These values describe the content. The headings and list formatting belong in the template introduced below.
JSON Schema describes which fields and values are accepted. The note's schema requires a title and summary as strings, and changes as an array of strings:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"title": { "type": "string" },
"summary": { "type": "string" },
"changes": { "type": "array", "items": { "type": "string" } }
},
"required": ["title", "summary", "changes"],
"additionalProperties": false
}Declaring a property in properties describes its value. Listing it in
required makes its presence mandatory. additionalProperties: false rejects
fields that the schema does not declare.
A template combines fixed Markdown with values from the JSON input.
Templates use MiniJinja syntax: {{ ... }} inserts a value, and {% ... %}
controls a loop or condition. JSON object properties become template variables.
# {{ title }}
{{ summary }}
## Changes
{% for change in changes %}
- {{ change }}{% endfor %}The library requires Rust 1.98 or later. Add these dependencies to your Cargo.toml:
[dependencies]
json2md = "0.1"
serde_json = "1.0"See the API reference for public types and methods.
A View compiles one schema and one template. Its render method validates
the input before executing the template, then returns the complete Markdown string.
The complete runnable example embeds the release-note files:
use json2md::View;
use serde_json::Value;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let schema: Value = serde_json::from_str(include_str!("assets/note.schema.json"))?;
let input: Value = serde_json::from_str(include_str!("assets/note.json"))?;
let view = View::new(&schema, include_str!("assets/note.j2"))?;
print!("{}", view.render(&input)?);
Ok(())
}Run it from the repository root:
cargo run --locked -p json2md --example renderStandard output matches the expected Markdown, including its final newline:
# Release 0.1
JSON data becomes a checked Markdown document.
## Changes
- Validate input before rendering.
- Reuse compiled schemas and templates.Keep the View to render more notes without compiling the schema and template again.
Each input is still validated. The view owns its compiled assets, so the original
schema and template can be dropped.
Input that fails the schema returns Error::Validation with field paths and diagnostics.
Using the same view, replace the title with a number:
use json2md::Error;
use serde_json::json;
let invalid = json!({
"title": 42,
"summary": "JSON data becomes a checked Markdown document.",
"changes": []
});
match view.render(&invalid) {
Ok(markdown) => println!("{markdown}"),
Err(Error::Validation(errors)) => {
for issue in errors.issues() {
eprintln!("{}: {}", issue.instance_path(), issue);
}
}
Err(error) => eprintln!("{error}"),
}The diagnostic for this input is:
/title: 42 is not of type "string"
The /title path identifies the rejected property. No template execution occurs
for this input. Match typed error variants to handle failures; diagnostic wording
and issue ordering can change with dependencies.
Use view.validate(&input) when you only need validation. Rendering always
validates; there is no unchecked rendering method.
The note currently requires summary. Remove it from the schema's required
array to make it optional.
The original template's {{ summary }} then fails with Error::Render when that
property is absent. Guard the optional section explicitly:
{% if summary is defined %}
{{ summary }}
{% endif %}Some MiniJinja filters and tests handle missing values without an error.
Keep a property required in the schema when every document must supply it.
A present JSON null differs from an absent property; this note's string schema
rejects null for summary.
View::new checks the schema and template separately. It does not prove that
the template can render every input accepted by the schema.
The renderer preserves Markdown and HTML characters without automatic escaping.
For example, a JSON string containing **bold** produces **bold** in the output.
Choose how to escape or sanitize values for your Markdown renderer.
The final template newline is preserved.
Library callers load files and write output. Schema references within the supplied document are supported; external schema retrieval and template file loading are disabled. Known schema formats are validated. Compile-time schema-template compatibility checking is outside v0.1. See the architecture contract for schema drafts, numeric limits, errors, and trust boundaries.
The command-line tool uses the same library to render files. Download a prebuilt binary from GitHub Releases. See binary installation for platforms and checksum verification.
Or install the json2md-cli package from crates.io with Rust 1.98 or later.
Its binary is named json2md:
cargo install --locked json2md-cliSupply your JSON, schema, and template files. The schema and template are required:
json2md render input.json --schema schema.json --template view.j2Markdown goes to standard output. Use --output PATH to write a file instead:
json2md render input.json --schema schema.json --template view.j2 --output document.mdRun the same release-note example from the repository root without installing:
cargo run --locked -p json2md-cli -- render examples/assets/note.json \
--schema examples/assets/note.schema.json --template examples/assets/note.j2Every render validates its input. Failures produce diagnostics on standard error and a nonzero exit status. Validation or rendering failures produce no Markdown. An unsuccessful operation preserves an existing output file. Output cannot replace an input, schema, or template file. See the CLI contract for output and diagnostic details.
Repository commands run from the workspace root. To install the CLI from source,
use cargo install --locked --path crates/json2md-cli.
The library integration tests compare real template files against independently written Markdown files. They also exercise invalid schemas, invalid data, strict missing values, schema references, formats, and view reuse. The CLI integration tests invoke the binary with real files. They check exact output, diagnostics, failure status, and preservation of inputs and existing output.
cargo fmt --all -- --check
cargo test --locked --workspace --all-targets
cargo test --locked --workspace --doc
cargo clippy --locked --workspace --all-targets -- -D warningsGenerate API documentation with cargo doc --locked --workspace --no-deps.
The CI workflow also compares library examples and CLI
output with committed Markdown files.
The GitLab fixtures also exercise finding and
summary bodies as offline regression tests. They are excluded from release packages.
See agent skills for repository authoring guidance and
README guidance for documentation examples.
See publishing for package verification and release commands.
Licensed under MIT OR Apache-2.0, at your choice. See LICENSE for both texts.