# json-to-csv-guide — JSON to CSV converter behaviour matrix

Reproducible data behind the article `json-to-csv-guide` (ja / en / es / pt).

The same 15 JSON inputs (`cases.json`) are pushed through four converters and the
exact CSV each one emits is recorded in `results.json`. The article quotes those
outputs verbatim; nothing in the article is estimated.

## Converters

| Converter | Invocation |
|---|---|
| FormatArc | `runToolConversion("json-to-csv", input, "en")` from `lib/tooling.ts` (PapaParse `unparse`) |
| json-2-csv (npm) | `json2csv(JSON.parse(input))`, default options |
| Miller | `mlr --ijson --ocsv cat` |
| pandas | `pandas.json_normalize(data).to_csv(index=False)` |

## Reproduce

```bash
# 1. npm deps for the JS converter (run inside this directory)
cd scripts/benchmarks/json-to-csv-guide && npm install && cd -

# 2. optional: Miller and pandas. Missing binaries are recorded as
#    "(not installed)" instead of failing the run.
brew install miller
python3 -m venv /tmp/pandas-venv && /tmp/pandas-venv/bin/pip install pandas

# 3. run (from the repository root)
PANDAS_PYTHON=/tmp/pandas-venv/bin/python npx tsx scripts/benchmarks/json-to-csv-guide/measure.ts
```

`measure.ts` rewrites `results.json` and prints every case to stdout.

## Environment of the committed results.json

Recorded in `results.json` under `environment`. The committed run was produced on
Apple M5 Pro / macOS (darwin arm64) with:

- Node v26.3.1
- FormatArc `lib/tooling.ts` (PapaParse 5.5.2)
- json-2-csv 5.5.11
- Miller (`mlr`) 6.19.0
- pandas 3.0.5 (CPython 3.14, isolated venv)

## What the matrix shows

Cases where all four converters agree:

- `C2` / `C8` nested objects flatten to dot-notation columns (`address.city`)
- `C9` a single top-level object becomes a one-row CSV
- `C10` commas, double quotes and embedded newlines are quoted the same way,
  matching the quoting rules of [RFC 4180](https://www.rfc-editor.org/rfc/rfc4180)
  section 2. The record separator differs: FormatArc emits CRLF (PapaParse
  default), the other three emit LF
- `C15` values starting with `=` are written through unescaped by all four, so
  the spreadsheet, not the converter, decides whether to evaluate them
  ([OWASP CSV injection](https://owasp.org/www-community/attacks/CSV_Injection))

Cases where they disagree (this is the point of the matrix):

- `C3` / `C4` arrays: JSON text in one cell (FormatArc, json-2-csv) vs indexed
  columns `tags.1`, `tags.2` (Miller) vs Python `repr` with single quotes (pandas)
- `C5` keys missing on some records: empty cells (FormatArc, pandas) vs the
  literal string `undefined` (json-2-csv) vs a hard `CSV schema change` error (Miller)
- `C6` `null`: empty cell (FormatArc, pandas) vs the literal text `null`
  (json-2-csv, Miller)
- `C7` empty object `{}`: empty cell plus a separate `meta.source` column
  (FormatArc) vs literal `{}` (json-2-csv) vs error (Miller) vs the column being
  dropped entirely (pandas)
- `C11` array of primitives: an explicit localized error message (FormatArc) vs
  silent blank rows (json-2-csv) vs an exception (Miller, pandas)
- `C12` empty array: an explicit localized error message (FormatArc) vs empty
  output with no error (json-2-csv, Miller, pandas)
- `C13` a literal dot inside a key name (`{"a.b":1,"a":{"b":2}}`): FormatArc,
  Miller and pandas all collapse both onto one `a.b` column and keep only the
  later value; json-2-csv escapes the literal key as `a\.b` and keeps both
- `C14` integers beyond 2^53: the JavaScript converters (FormatArc, json-2-csv)
  round them during `JSON.parse` (`9007199254740993` becomes `9007199254740992`);
  Miller (Go) and pandas keep them exact

`node_modules/` is gitignored (`.gitignore` line `scripts/benchmarks/**/node_modules/`);
`package.json` + `package-lock.json` pin the JS converter version.
