Data formats · Beginner guide
CSV, JSON, or Markdown: Which Format Is Better for LLM Prompts?

The best format depends on the question you are asking, the structure of the data, and how much room your prompt has. For flat rows, CSV is compact. For nested records, JSON is explicit. For data that a person must inspect, Markdown tables are readable. The difficult part is converting between them without silently changing the data.
Short answer
Use CSV when your data is a rectangular collection of rows and columns. Use JSON when records contain arrays, objects, optional fields, or other nested values. Use Markdown when a reader needs to scan the result directly inside a prompt or answer.
There is no universal winner. A useful default is to preserve the source format during storage and processing, then render a compact view for the model. That view might be CSV for a large flat dataset, JSON for structured extraction, or a Markdown table for a small human-reviewed sample.
| Situation | Start with | Reason |
|---|---|---|
| Flat records with simple scalar values | CSV | Usually compact and easy to generate. |
| Nested objects, arrays, or optional fields | JSON | Structure is explicit instead of being encoded into cell text. |
| Small result that people must inspect | Markdown | Readable in a prompt, issue, or documentation page. |
These are format recommendations, not measured claims about every model or dataset. The script below lets you measure your own payload using the same input and encoding.
A concrete case: support tickets with awkward values
Imagine a support export with these columns:
ticket_id, customer_note, tags,
assignee, and metadata.
A note may contain a comma, a quotation mark, or a line break. The tags may be a list. The metadata may be an object such as a browser name and a priority. This is exactly where a quick string replacement becomes unsafe.
ticket_id,customer_note,tags,assignee,metadata
T-104,"The export failed, then worked again
after a retry.","[""export"",""retry""]",Mira,"{""priority"":""high"",""browser"":""Firefox""}"
The comma in the note is data, not a column separator. The line break is data, not the end of the record. The quotes around the list and object are CSV quoting, not necessarily part of the values. A correct parser must understand all three distinctions.
If the same information is represented as JSON, its structure is more visible:
[
{
"ticket_id": "T-104",
"customer_note": "The export failed, then worked again\nafter a retry.",
"tags": ["export", "retry"],
"assignee": "Mira",
"metadata": {
"priority": "high",
"browser": "Firefox"
}
}
]
A Markdown table can be useful for a human, but nested values still need a policy. You can serialize them as JSON inside a cell, flatten selected fields into separate columns, or keep the record as JSON instead of forcing it into a table.
How CSV, JSON, and Markdown differ
CSV: compact rows with a strict dialect
CSV represents a table. Its apparent simplicity hides several configuration choices: delimiter, quote character, escape behavior, line ending, header presence, and encoding. A file can be valid CSV while still being incompatible with a reader that assumes a different dialect.
CSV is a strong choice when every record has the same set of scalar columns. It becomes less convenient when a cell contains a list or object. Those values can be serialized, but the table then contains a second data language inside a cell.
JSON: explicit structure and types
JSON can represent objects, arrays, strings, numbers, booleans, and null. That makes it a natural fit for nested records and tool inputs. It also makes the boundaries between values explicit, provided the parser is used rather than treating JSON as plain text.
JSON often repeats field names. Pretty-printed JSON also includes indentation and line breaks. Compact JSON reduces whitespace, but it does not remove the structural punctuation or repeated keys.
Markdown: a presentation layer
Markdown is primarily a text-formatting convention. A Markdown table uses pipes to separate cells and hyphens to define the header separator. A value containing a pipe, a line break, or HTML-like text therefore needs careful escaping or a documented rendering policy.
Markdown can be excellent for a small, readable excerpt. It is a risky canonical representation because it does not have one universal table parser with one universal escaping policy. Treat it as a view over structured data.
| Dimension | CSV | JSON | Markdown table |
|---|---|---|---|
| Best shape | Flat rows | Nested records | Small rectangular view |
| Types | Usually inferred from text | Represented directly | Usually presented as text |
| Human scanability | Moderate | Moderate | High for small tables |
| Main failure risk | Incorrect quoting or dialect | Unexpected shape or serialization policy | Unescaped pipes and line breaks |
Safe conversion to Markdown
A safe conversion has two separate stages:
- Parse the source with a format-aware parser.
- Render each parsed value using an explicit Markdown policy.
Do not split CSV rows with line.split(","). Do not split Markdown rows with
line.split("|") unless you have already defined how escaped pipes and
code spans are handled. Do not flatten JSON by recursively concatenating strings without
deciding what happens to arrays, nulls, and objects.
Escaping a Markdown cell
At minimum, a table renderer should escape the table delimiter and convert embedded
line breaks into a visible, deterministic representation. The script below uses
\| for a literal pipe and the two-character sequence
<br> for a line break. That is a rendering decision; change it if
your Markdown consumer uses another convention.
Handling nested fields
There are three reasonable policies:
- Keep nested values in one cell as compact JSON.
- Flatten selected paths, such as
metadata.priority. - Do not convert nested records to a table; send JSON instead.
The first policy preserves more information with less schema design. The second makes a known query easier to read. The third avoids pretending that a tree is a rectangle.
Dependency-free Python script
Save the following as compare_formats.py. It uses only Python's standard
library. It reads CSV with csv.DictReader, parses JSON with
json.loads, renders a Markdown view, and reports UTF-8 byte lengths.
#!/usr/bin/env python3
import csv
import io
import json
import sys
from pathlib import Path
from typing import Any
def markdown_escape(value: Any) -> str:
if value is None:
text = ""
elif isinstance(value, (dict, list)):
text = json.dumps(value, ensure_ascii=False, separators=(",", ":"))
else:
text = str(value)
return (
text.replace("\\", "\\\\")
.replace("|", "\\|")
.replace("\r\n", "<br>")
.replace("\n", "<br>")
.replace("\r", "<br>")
)
def csv_to_records(csv_text: str) -> list[dict[str, Any]]:
reader = csv.DictReader(io.StringIO(csv_text, newline=""))
if reader.fieldnames is None:
raise ValueError("CSV input has no header row")
return [dict(row) for row in reader]
def json_to_records(json_text: str) -> list[dict[str, Any]]:
value = json.loads(json_text)
if not isinstance(value, list):
raise ValueError("Expected a JSON array of records")
if not all(isinstance(item, dict) for item in value):
raise ValueError("Every JSON array item must be an object")
return value
def records_to_markdown(records: list[dict[str, Any]]) -> str:
if not records:
return ""
columns = list(records[0].keys())
lines = [
"| " + " | ".join(markdown_escape(column) for column in columns) + " |",
"| " + " | ".join("---" for _ in columns) + " |",
]
for record in records:
cells = [markdown_escape(record.get(column)) for column in columns]
lines.append("| " + " | ".join(cells) + " |")
return "\n".join(lines) + "\n"
def utf8_size(text: str) -> int:
return len(text.encode("utf-8"))
def report(name: str, text: str) -> None:
print(f"{name:10} {utf8_size(text):8} bytes")
def main() -> int:
if len(sys.argv) != 3:
print("Usage: python compare_formats.py data.csv data.json", file=sys.stderr)
return 2
csv_path = Path(sys.argv[1])
json_path = Path(sys.argv[2])
csv_text = csv_path.read_text(encoding="utf-8", newline="")
json_text = json_path.read_text(encoding="utf-8")
csv_records = csv_to_records(csv_text)
json_records = json_to_records(json_text)
csv_as_json = json.dumps(
csv_records, ensure_ascii=False, separators=(",", ":")
)
json_compact = json.dumps(
json_records, ensure_ascii=False, separators=(",", ":")
)
markdown = records_to_markdown(csv_records)
print("Input record counts:")
print(f"CSV {len(csv_records)}")
print(f"JSON {len(json_records)}")
print()
print("UTF-8 encoded sizes:")
report("CSV input", csv_text)
report("JSON input", json_text)
report("CSV->JSON", csv_as_json)
report("JSON compact", json_compact)
report("Markdown", markdown)
if csv_records != json_records:
print()
print("WARNING: parsed CSV and JSON records differ", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
The script compares two inputs as parsed records before comparing their rendered sizes. That check matters: a size comparison is not useful if the CSV and JSON files do not contain the same data.
Why the script preserves line breaks while reading CSV
The file is opened with a newline setting that lets Python's CSV parser recognize embedded line breaks correctly. The parser, rather than a manual line loop, decides where a record ends.
Why nested values are serialized explicitly
The Markdown renderer turns lists and objects into compact JSON inside one cell. This keeps the conversion deterministic and avoids Python-specific representations such as single-quoted dictionary keys.
Reproducible size comparison
To reproduce the measurement, create these two files. They are demonstration inputs, not production data:
ticket_id,customer_note,tags,assignee,metadata
T-104,"The export failed, then worked again
after a retry.","[""export"",""retry""]",Mira,"{""priority"":""high"",""browser"":""Firefox""}"
T-105,"The customer wrote: ""please retry""; the second attempt passed.","[""retry""]",Oleg,"{""priority"":""normal"",""browser"":""Chrome""}"
T-106,"A pipe | appears in the note.","[""delimiter""]",Mira,"{""priority"":null,""browser"":""Safari""}"
[
{
"ticket_id": "T-104",
"customer_note": "The export failed, then worked again\nafter a retry.",
"tags": ["export", "retry"],
"assignee": "Mira",
"metadata": {"priority": "high", "browser": "Firefox"}
},
{
"ticket_id": "T-105",
"customer_note": "The customer wrote: \"please retry\"; the second attempt passed.",
"tags": ["retry"],
"assignee": "Oleg",
"metadata": {"priority": "normal", "browser": "Chrome"}
},
{
"ticket_id": "T-106",
"customer_note": "A pipe | appears in the note.",
"tags": ["delimiter"],
"assignee": "Mira",
"metadata": {"priority": null, "browser": "Safari"}
}
]
Run the script from the directory containing the files:
python3 compare_formats.py tickets.csv tickets.json
The output will contain byte counts for the exact files you supplied. This article does not report fixed numeric results because those values depend on the exact whitespace, line endings, encoding, field names, and data volume in your inputs.
Measure the right thing
Use the same encoding for every representation. The script uses UTF-8 bytes. Character count and byte count are not always equal, especially when values contain non-ASCII characters.
Keep these variants separate:
- Original CSV, including its header, quoting, and line endings.
- Original JSON, including its indentation and final newline.
- Compact JSON generated with separators that remove optional whitespace.
- Markdown generated by the stated escaping policy.
Comparing pretty JSON with compact CSV answers a different question from comparing normalized formats. Record the normalization rules alongside the size.
Verification and failure cases
Check that parsing preserved the records
The script compares the parsed CSV records with the parsed JSON records. If the values differ, it returns a non-zero exit status and prints a warning. That check catches common fixture mistakes, such as a missing row or a field represented differently.
Test delimiters inside values
Add commas, semicolons, and pipes to notes. CSV commas must be quoted according to the CSV dialect. Markdown pipes must be escaped by the renderer. A correct output should still have the same number of parsed records and columns.
Test embedded newlines
Put a real line break inside a quoted CSV field. The CSV parser should keep it inside
the field. The Markdown output should use the renderer's explicit line-break policy.
If a downstream Markdown consumer interprets <br> differently, change
the policy and test that consumer.
Test quotes
Include a double quote in a CSV value. Standard CSV represents it by doubling the quote inside a quoted field. A manual comma split will not understand that rule.
Test empty and null-like values
CSV has text fields, so an empty cell is not automatically the same as JSON
null. Decide whether empty strings, missing keys, and nulls are distinct in
your application. The sample renderer displays both an empty string and
null as an empty Markdown cell, which is readable but not reversible without
the original structured data.
Test inconsistent JSON shapes
The script expects a JSON array whose items are objects. It deliberately rejects a single object, a scalar, or an array containing non-object items. If your input uses a different shape, adapt the validation instead of silently coercing it.
Limitations
This comparison measures serialized byte size, not model quality, latency, cost, or retrieval accuracy. A smaller representation may be harder for a model or person to interpret. A larger representation may preserve useful structure and reduce ambiguity.
The script assumes UTF-8 input, a header row in CSV, a JSON array of objects, and one chosen Markdown table policy. It does not implement every CSV dialect, Markdown extension, schema validation rule, or model-specific tokenizer.
Token counts can differ from byte counts. If prompt budget is the deciding factor, run the same fixtures through the tokenizer used by your target model and record that measurement separately. Do not infer token usage from byte size alone.
Finally, conversion can lose semantics. CSV cannot distinguish an empty string from a missing field without an additional convention. Markdown tables do not naturally preserve nested structure. JSON can preserve structure but may repeat keys and include more punctuation. Keep the source representation when reversibility matters.
Conclusion
For most prompt workflows, the practical answer is conditional: CSV for compact flat data, JSON for nested data, and Markdown for a small human-readable view. Parse with a real parser, define an escaping policy, compare equivalent records, and measure the exact representations you intend to send.
The safest architecture is often “structured source, rendered prompt view.” That keeps data integrity separate from presentation and makes failures testable.
Continue with the guides or look up related terms in the glossary.