How to convert json release data to a markdown changelog table
- Step 1Shape releases as a JSON array of objects — One release per object:
[{ "version": "2.1.0", "date": "2026-06-01", "type": "minor", "summary": "Added dark mode" }, ...]. Keys become columns in first-seen order — list them as you want the changelog columns (Version, Date, Type, Summary is a common order). - Step 2Open the tool and drop the file — Drop a
.json,.ndjson,.jsonl, or.txtfile — or an.xlsx/.xlssheet, whose first tab is read into a JSON array first. All parsing happens locally. - Step 3Pick alignment — Left (
:---) suits text-heavy summaries. Center (:---:) works for a shorttypecolumn. Right (---:) is unusual for changelogs. Alignment applies to all columns at once. - Step 4Click Convert to Markdown — The tool emits a header row, an alignment row, and one body row per release. Newest-first or oldest-first ordering is whatever order your array is in — the tool does not sort. Sort the array beforehand if you want newest-first.
- Step 5Review the rendered table — Check the preview (on-screen view truncates very long output at 5,000 characters; Copy/Download keep the full text). Confirm versions are in the order you want and the summary column reads cleanly.
- Step 6Copy or download into CHANGELOG.md — Copy or Download MD, then paste under a
# Changelogheading you write. The tool produces the table; you add the heading and any release-cadence note.
What this tool does — and what it does NOT
A direct correction of common expectations. JSON to Markdown produces a single pipe table.
| You might expect | Reality | What to do instead |
|---|---|---|
Keep a Changelog ## [x.y.z] - date headings | Not produced — output is one table | Use a version-history table, or write the headings manually |
| Grouped Added / Changed / Fixed / Removed lists | Not produced — no nested lists | Add a type column and one row per change |
| Automatic newest-first sorting | No sorting — array order is preserved | Sort the JSON array before converting |
| Per-column alignment | Single alignment for the whole table | Choose Left for changelog tables |
| One row per change, grouped by version | Supported — make each change an object with a version column | [{version, type, change}, ...] |
Two changelog table layouts
Both are valid; pick the JSON shape that matches the layout you want.
| Layout | JSON shape | Resulting columns |
|---|---|---|
| One row per release | [{version, date, type, summary}, ...] | version, date, type, summary |
| One row per change | [{version, type, change}, ...] | version, type, change (version repeats) |
| Release with PR links | [{version, change, pr}, ...] | version, change, pr |
Tier limits
Real configured numbers. JSON to Markdown is a Pro tool; free is size-gated.
| Limit | Free | Pro |
|---|---|---|
| File size | 2 MB | 100 MB |
| Files per run | 1 | 10 (bulk) |
| Processing | Browser | Browser |
Cookbook
Real release arrays turned into changelog tables, with the exact Markdown emitted.
Version history → one-row-per-release table
ExampleThe standard changelog matrix. Sort your array newest-first before converting — the tool preserves array order and does not sort.
Input (releases.json, newest first):
[
{ "version": "2.1.0", "date": "2026-06-01", "type": "minor", "summary": "Added dark mode" },
{ "version": "2.0.1", "date": "2026-05-20", "type": "patch", "summary": "Fixed export crash" }
]
Output (alignment: left):
| version | date | type | summary |
| :--- | :--- | :--- | :--- |
| 2.1.0 | 2026-06-01 | minor | Added dark mode |
| 2.0.1 | 2026-05-20 | patch | Fixed export crash |One row per change, grouped by version column
ExampleTo approximate Keep a Changelog's Added/Fixed grouping inside a single table, make each change its own object with version and type columns. The version repeats down the rows.
Input:
[
{ "version": "2.1.0", "type": "Added", "change": "Dark mode toggle" },
{ "version": "2.1.0", "type": "Fixed", "change": "Crash on empty export" },
{ "version": "2.0.1", "type": "Fixed", "change": "Timezone offset" }
]
Output:
| version | type | change |
| :--- | :--- | :--- |
| 2.1.0 | Added | Dark mode toggle |
| 2.1.0 | Fixed | Crash on empty export |
| 2.0.1 | Fixed | Timezone offset |Summary containing a pipe is escaped
ExampleA summary mentioning an option like --format a|b would otherwise split the row. The converter backslash-escapes the pipe so the row keeps its column count.
Input:
[{ "version": "3.0", "summary": "Support --format a|b flag" }]
Output:
| version | summary |
| :--- | :--- |
| 3.0 | Support --format a\|b flag |Optional breaking-change column
ExampleMark only the releases that break compatibility. The breaking column appears for all rows, blank where the key is absent.
Input:
[
{ "version": "3.0.0", "summary": "New config schema", "breaking": true },
{ "version": "2.9.0", "summary": "Perf improvements" }
]
Output:
| version | summary | breaking |
| :--- | :--- | :--- |
| 3.0.0 | New config schema | true |
| 2.9.0 | Perf improvements | |Multi-line release note collapsed to one row
ExampleA summary with an embedded newline can't span table rows. The converter replaces the newline with a space, keeping the release on one line. Keep long prose out of the table.
Input:
[{ "version": "4.0", "summary": "Rewrote core.\nMigration required." }]
Output:
| version | summary |
| :--- | :--- |
| 4.0 | Rewrote core. Migration required. |Errors and edge cases
Real errors and silent failures sourced from each platform's own documentation. Match the wording to the row, fix what the row says to fix.
Expecting Keep a Changelog headings and sub-lists
Not supportedThis tool cannot emit ## [x.y.z] - date headings or nested Added/Changed/Fixed bullet lists — it produces a single pipe table. For a true Keep a Changelog file, use a table layout (one row per change with a type column) or generate the headings with a dedicated changelog generator. The table this tool makes is a valid, widely-used changelog style on its own.
Releases come out in the wrong order
No sortingThe tool preserves the array order exactly — it does not sort by version or date. If your output is oldest-first but you want newest-first, sort the JSON array before converting (most release tooling can export in the order you want).
Each release is a separate JSON file
Needs mergingIf your tooling writes one file per release, combine them into a single array first with json-object-merger (or a small script), then convert. The tool reads one file per free run.
Release object has nested change lists
Inlined JSONA shape like { "version": "2.0", "changes": ["a", "b"] } puts the whole changes array, JSON-stringified, into one cell (["a","b"]). For a readable per-change layout, expand to one object per change before converting.
Invalid JSON from a hand-edited file
Parse errorJSON.parse rejects trailing commas, comments, and single quotes — common in hand-maintained release files. Run it through json-format-fixer, or pinpoint the error with json-validator, then re-convert.
Pipe inside a version range or summary
PreservedA value containing | (a flag name, a regex) is backslash-escaped to \| so the table structure holds. It renders with the pipe visible. This is automatic and not toggleable in the UI.
Empty array
Header-onlyNo release objects means no keys, so no columns are produced. Include at least one release object so headers can be detected.
Date stored as a number (epoch)
StringifiedAn epoch timestamp like 1717200000 renders verbatim as the number — it is NOT reformatted to a readable date. Convert epochs to ISO date strings in your data before converting if you want human-readable dates.
File over 2 MB on free
Blocked on freeFree caps input at 2 MB. A very long history with verbose summaries can approach it. Trim older releases or upgrade to Pro (100 MB).
Frequently asked questions
Does this output Keep a Changelog format?
No. It outputs a single GitHub-flavored Markdown table, not ## [version] - date headings with grouped Added/Changed/Fixed lists. The earlier description was wrong about this. You can still build a perfectly good changelog as a version-history table — many projects use exactly that format.
Can I get Added / Changed / Fixed grouping?
Only as a column, not as separate headed sub-lists. Make each change its own object with a type column (Added, Fixed, etc.) and a version column; the table then shows the change type per row. Nested grouped lists are beyond what a pipe-table converter can produce.
Will it sort releases newest-first automatically?
No. The tool preserves your array order exactly. Sort the JSON array before converting if you want a specific order — the converter never reorders rows.
How are columns chosen?
Automatically, as the union of all keys across the release objects, in first-seen order. To control column order, define all keys in your desired order in the first object.
Can I align columns individually?
No — one alignment applies to the whole table (Left, Center, or Right). Left is the right choice for changelog tables.
Are dates reformatted?
No. Date strings render exactly as stored (2026-06-01 stays 2026-06-01); an epoch number stays a number. Store dates in the display format you want before converting.
What if a release has a nested array of changes?
It's JSON-stringified into one cell. For a readable layout, expand to one object per change (with a repeated version column) before converting.
How do I combine per-release JSON files?
Merge them into one array first with json-object-merger or a small script, then convert the combined file. The tool processes one file per free run.
What inputs are accepted?
.json, .ndjson, .jsonl, .txt, plus .xlsx/.xls. A spreadsheet's first sheet is read into a JSON array of objects before the table is built.
Is unreleased version data uploaded?
No. Conversion is fully client-side — release data, including unreleased entries, never leaves your browser. Only an anonymous run count is recorded if you're signed in.
How big can the changelog file be?
2 MB on free, 100 MB on Pro. Even long histories rarely approach the free cap.
Can I regenerate the changelog table in CI?
Yes — it's a Pro tool with API access. Call it via the local runner in your release workflow to rebuild the table from your release JSON. Conversion runs locally, so the data stays on your machine.
Privacy first
Conversion runs locally in your browser. No file is uploaded — only metadata counters are saved for signed-in dashboard stats.