How to format json data records as a markdown table
- Step 1Assemble representative records — Pick a few records that exercise the full schema — including ones with optional/nullable fields populated — so the union of keys covers every column you want to document.
- Step 2Anonymize if needed — If records contain PII or secrets, anonymize or run md-secret-redactor before pasting. The tool converts exactly what you give it.
- Step 3Flatten nested fields — Nested objects render as
[object Object]. Promote the sub-fields you want as columns to top level (e.g.meta.owner→owner) before converting. - Step 4Paste and Run — Paste the array (or upload the
.json). There are no options — Run produces the table immediately, with columns from every record's keys. - Step 5Verify the column set against your schema — Cross-check the headers against your documented field list. A missing column means no sample record included that field — add one that does if it needs documenting.
- Step 6Embed in the dictionary or spec — Paste the table into your data dictionary. To publish it as a page, convert with md-to-html; for a doc deliverable, md-to-docx.
Record field types in the output table
How each kind of data-dictionary value renders. Every cell is String(value ?? "").
| Field kind | Example value | Cell output |
|---|---|---|
| Identifier (string) | "ORD-1042" | ORD-1042 |
| Quantity (number) | 12 | 12 |
| Flag (boolean) | false | false |
| Date string | "2026-06-13" | 2026-06-13 |
| Nullable field set to null | null | (empty cell) |
| Optional field absent | (key missing) | (empty cell) |
| Nested object | {"ccy":"USD"} | [object Object] — flatten first |
Union-of-keys behavior for documentation
Why heterogeneous record sets still produce a complete dictionary table. Columns appear in first-seen order.
| Records | Resulting columns | Empty cells where |
|---|---|---|
| All identical keys | Exactly those keys | Only where values are null/missing |
| Record 2 adds an optional key | All keys, union | Record 1's row, optional column |
| Records use different keys entirely | Every key from every record | Each row, columns it lacks |
| A record is a primitive (not object) | Keys from object records only | Entire primitive row |
Tier limits
Markdown-family caps. Documentation samples are typically tiny; uploads matter for whole exported datasets.
| Tier | Upload size | Paste characters | Files/run |
|---|---|---|---|
| Free | 1 MB | 500,000 | 1 |
| Pro | 10 MB | 5,000,000 | 10 |
| Pro-media | 50 MB | 20,000,000 | 50 |
| Developer | 500 MB | unlimited | unlimited |
Cookbook
Record samples a data dictionary or spec would use, with the exact tables they produce. Sample data is anonymized.
A uniform record set
When every record shares the same fields, you get a tidy dictionary table — one column per field, one row per record.
Input:
[
{"field": "order_id", "type": "string", "required": true},
{"field": "total", "type": "number", "required": true},
{"field": "note", "type": "string", "required": false}
]
Output:
| field | type | required |
| --- | --- | --- |
| order_id | string | true |
| total | number | true |
| note | string | false |Optional/nullable fields shown as blanks
Mark optional fields by leaving them out or setting null in some records — they render as empty cells, which reads as 'nullable' in a dictionary.
Input:
[
{"id": 1, "closedAt": "2026-06-01"},
{"id": 2, "closedAt": null},
{"id": 3}
]
Output:
| id | closedAt |
| --- | --- |
| 1 | 2026-06-01 |
| 2 | |
| 3 | |Documenting a default value
Boolean and numeric defaults render as readable text, making a 'default' column in a config dictionary clear at a glance.
Input:
[
{"setting": "retries", "default": 3, "enabled": true},
{"setting": "verbose", "default": false, "enabled": false}
]
Output:
| setting | default | enabled |
| --- | --- | --- |
| retries | 3 | true |
| verbose | false | false |Flatten a nested type descriptor
A nested type object becomes [object Object]. Promote its fields to top level so the dictionary shows them.
Raw:
{"field": "amount", "type": {"base": "decimal", "scale": 2}}
Flatten to:
[{"field": "amount", "type.base": "decimal", "type.scale": 2}]
Output:
| field | type.base | type.scale |
| --- | --- | --- |
| amount | decimal | 2 |A path value with a pipe stays intact
Spec records often include example paths or expressions containing |. The pipe is escaped so the cell isn't split.
Input:
[{"field": "route", "example": "/a/{id} | /a/all"}]
Output:
| field | example |
| --- | --- |
| route | /a/{id} \| /a/all |Edge cases and what actually happens
Nested field descriptors show as [object Object]
By designIf a record's value is itself an object (a nested type or constraint), it renders as [object Object] because the tool uses String(). Flatten the descriptor into top-level keys, or give it a separate sub-table in the dictionary.
Constraint arrays collapse to comma text
By designAn enum: ["a","b","c"] field becomes a,b,c via String(). That's often acceptable for a dictionary, but nested arrays lose structure. Pre-serialize complex constraints as strings if you need exact formatting.
Invalid JSON
Invalid JSON inputTrailing commas, comments, single quotes, or an incomplete fragment cause JSON.parse to fail and the tool returns Invalid JSON input.. Validate the sample first; it must be one well-formed JSON document.
Empty record set
Empty arrayAn input of [] returns Empty array.. Provide at least one record. For documenting the empty state itself, describe it in prose.
Records are strings, not objects
No object keys foundAn array of bare strings or numbers (["id","total"]) returns No object keys found. because there are no keys. Wrap each in an object — [{"field":"id"}] — to make a column.
Free-text field contains a newline
Row breakA multi-line description value breaks the row because newlines aren't escaped (only | is). Replace newlines with spaces in the sample, or fix alignment afterward with md-table-repair.
Inconsistent key casing across records
Two columnsisActive in one record and is_active in another become two separate columns under the union rule. Normalize key names in the sample so the dictionary shows a single column per field.
Wide record schema
ExpectedRecords with many fields create very wide tables. The tool does not wrap or paginate columns. Trim to the fields being documented, or split a large schema into grouped dictionary tables.
PII left in the sample
Privacy riskThe tool converts verbatim — real names, emails, or secrets will appear in the published dictionary. Use anonymized sample records, or run md-secret-redactor for tokens, before converting.
Sample exceeds the paste limit
413 over limitThe paste box is capped at the tier's character limit (Free 500,000). For a large exported dataset, upload the .json instead (Free 1 MB) or trim to a representative sample — a dictionary rarely needs every row.
Frequently asked questions
How are columns chosen when records have different fields?
Columns are the union of every key across every record, in first-seen order. So an optional field present on only some records still gets a column; records lacking it show an empty cell. This is exactly what you want for documenting optional/nullable fields in a data dictionary.
How do null and missing fields look?
Both render as empty cells. An explicit null is coerced via ?? "" to empty, and an absent key reads as undefined and is also blank. The output never shows a literal 'null' from these — which reads cleanly as 'nullable/optional' in a spec.
What happens to nested objects in a record?
They render as [object Object], because the tool uses String() rather than JSON.stringify. Flatten the sub-fields you want to document into top-level keys (e.g. type.scale) before converting, or give the nested structure its own table.
Are date and number fields preserved?
Yes. Date strings are kept verbatim, and numbers/booleans are coerced to readable text. There's no date reformatting and no locale handling — values pass through as their string form.
Can I select which columns appear?
Not in the tool — there are no options at all. The column set is always the union of record keys. To control columns, remove unwanted keys from the records before converting, or trim the resulting Markdown table by hand.
Why did I get 'No object keys found.'?
Your array holds primitives (strings/numbers), not objects, so there are no keys to make columns. Wrap each value in an object — [{"field": "id"}] — to produce a one-column table.
How do I document constraints like an enum list?
An array value such as enum: ["a","b"] becomes a,b via String(). That's usually fine for a dictionary. For richer formatting, pre-serialize the constraint into a string field in your source data.
Is the data uploaded anywhere?
No. Conversion runs entirely in your browser tab — pre-release schemas and internal samples never reach a server. The only server write is an anonymous usage counter for signed-in dashboards, with no content.
What if a value contains a pipe or a newline?
Pipes are escaped to \| so they don't break columns. Newlines are not escaped, so a multi-line free-text value will break the row — strip newlines from such fields before converting.
How big a dataset can I convert?
Free tier allows a 1 MB upload or 500,000 pasted characters; Pro 10 MB / 5,000,000; Pro-media 50 MB / 20,000,000; Developer 500 MB / unlimited. For a dictionary you usually want a small representative sample, well under any cap.
Can I publish the dictionary table as a page?
Yes — send the Markdown to md-to-html for a web page, or md-to-docx for a Word deliverable. Both are markdown-family siblings.
My source is CSV, not JSON — what should I use?
Use csv-to-md-table; it parses CSV with PapaParse and has a header-row toggle. This JSON tool is for arrays of objects. If a converted table gets misaligned during editing, md-table-repair re-aligns it.
Privacy first
All Markdown processing runs locally in your browser using JavaScript. No file is ever uploaded to JAD Apps servers — only metadata counters are saved for signed-in dashboard stats.