Renders arbitrary JSON/Python data as readable, CommonMark-safe Markdown.
A Python port of MarkdownEncoder from
katmore/micro-encode (PHP).
jsonschema2md converts a JSON Schema — the shape a family of values is allowed to take — into prose documentation. Given this schema:
{
"type": "object",
"properties": {
"firstName": { "type": "string", "description": "The person's first name." }
}
}it produces:
- **`firstName`** *(string)*: The person's first name.There's no schema in mdencode at all. It renders an actual value you
already have:
to_markdown({"firstName": "Doug"})
# '- **firstName:** Doug'No schema to write, and nothing to keep in sync with the data as it changes.
json2md (npm) compiles a document you author block-by-block, choosing the Markdown shape yourself:
json2md([{ h1: "Report" }, { ul: ["a", "b"] }])
// => "# Report\n\n - a\n - b"mdencode takes no authorial input. It infers the rendering purely from an
existing value's own shape — a list is a list, a map is a map:
to_markdown({"name": "Doug", "things": ["foo", "bar"]})
# '- **name:** Doug\n- **things:**\n - foo\n - bar'A ten-minute recursive converter (array -> "- " bullets, dict -> "- **key:**" labels) looks fine on tidy fixtures and quietly corrupts real
data. Two examples, checked against an actual CommonMark parser:
A literal - in a string value. A naive converter emits it unescaped:
naive(["- item", "plain"])
# '- - item\n- plain'That second - isn't text to CommonMark — it's a new marker, so the
"item" note becomes a nested bullet list instead of a line of text:
<ul><li><ul><li>item</li></ul></li><li>plain</li></ul>mdencode escapes the leading - so it stays literal text:
to_markdown(["- item", "plain"])
# '- \\- item\n- plain'
# renders as: <ul><li>- item</li><li>plain</li></ul>A label directly above a nested list of objects. Naively joining a
"- **key:**" label to a nested list with a single \n produces:
- **releases:**
1.
- **tag:** 2.0.0
- **breaking:** true
2.
- **tag:** 1.0.0
- **breaking:** falseCommonMark doesn't parse the 1. as a list at all here — it gets absorbed
as literal paragraph text under releases:, and the two release entries
come out merged and mis-numbered. mdencode inserts a blank line whenever
a label is followed by a nested list that itself starts with a
content-less marker:
to_markdown({"releases": [
{"tag": "2.0.0", "breaking": True},
{"tag": "1.0.0", "breaking": False},
]})
# '- **releases:**\n\n 1.\n - **tag:** 2.0.0\n - **breaking:** true\n 2.\n - **tag:** 1.0.0\n - **breaking:** false'which parses as two clean, separate list entries.
mdencode also promotes a list to numbered markers (1., 2., ...)
whenever any element is itself an object, array, or multiline string,
regardless of the ordered_lists option — CommonMark can only nest a
bare, content-less marker safely under a numbered parent, never under
another bare -.
(Not in scope: XML/HTML encoding — the PHP original also ships
XmlEncoder/HtmlEncoder; this port is Markdown-only.)
pip install mdencodefrom mdencode import to_markdown
to_markdown({
"name": "Doug",
"active": True,
"things": ["foo", "bar"],
})- **name:** Doug
- **active:** true
- **things:**
- foo
- barPass ordered_lists=True to render plain-value lists as 1. foo instead
of - foo. A list containing anything other than plain values (a nested
object, array, or multiline string) always renders as an ordered list
either way, for the CommonMark-nesting reason above.
echo '{"name":"Doug","active":true}' | json2md- **name:** Doug
- **active:** trueConvert a file instead by passing its path:
json2md path/to/data.jsonAdd --ordered for numbered lists where that applies:
echo '{"tags":["php","markdown"]}' | json2md --orderedRun json2md --help for the full usage. Invalid input (bad JSON, an
unreadable file, or nothing on stdin) prints a plain-English error to
stderr and exits non-zero — never partial or broken output.
json2md expects one JSON document. Log files and session transcripts —
including Claude Code and
Codex session files — are usually
newline-delimited JSON instead: one independent JSON value per line.
jsonl2md renders each line as its own record, separated by a thematic
break (---), rather than treating the whole file as one structure:
jsonl2md path/to/session.jsonl | lesshead -3 ~/.claude/projects/*/some-session-id.jsonl | jsonl2md- **type:** queue-operation
- **operation:** enqueue
- **timestamp:** 2026-08-26T05:48:17.617Z
...
---
- **type:** queue-operation
- **operation:** dequeue
...
---
- **parentUuid:** null
- **type:** user
- **message:**
- **role:** user
- **content:** ...Blank lines are skipped; --ordered and -h/--help work the same as
json2md.
mdencode — https://github.com/katmore/mdencode
Copyright (c) 2026 Doug Bird. All Rights Reserved.
mdencode is copyrighted free software. You may redistribute and modify it under either the terms and conditions of the "The MIT License (MIT)"; or the terms and conditions of the "GPL v3 License". See LICENSE and GPLv3.