Data formats · Beginner guide

CSV, JSON, or Markdown: Which Format Is Better for LLM Prompts?

CSV, JSON или Markdown: какой формат лучше для промптов
Temporary fallback cover; replace in editorial pass.

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.

30 min read Level: from scratch Updated: 2 November 2026

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.

Practical starting point
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.

Comparison dimensions
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:

  1. Parse the source with a format-aware parser.
  2. 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.

We publish what works for us—and implement the same solutions for your business. We design AI automation, Telegram bots, chats, and AI agents for real-world processes. Discuss your project →