このページは FormatArc のブログ記事が引用している実測の生データです。検索結果には表示されません (noindex)。
| File | Size |
|---|---|
| README.md | 3.7 KB |
| cases.json | 2.4 KB |
| measure.ts | 4.9 KB |
| package-lock.json | 2.4 KB |
| package.json | 341 B |
| results.json | 8.0 KB |
# 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.
{
"generatedAt": "2026-07-26T04:13:19.274Z",
"environment": {
"node": "v26.3.1",
"platform": "darwin/arm64",
"converters": {
"FormatArc": "lib/tooling.ts runToolConversion('json-to-csv') — papaparse 5.5.2",
"json-2-csv": "json-2-csv 5.5.11",
"Miller": "mlr 6.19.0",
"pandas": "pandas 3.0.5"
}
},
"results": [
{
"id": "C1",
"label": "Flat array of objects (baseline)",
"input": "[{\"name\":\"Mika\",\"role\":\"admin\"},{\"name\":\"Noah\",\"role\":\"viewer\"}]",
"outputs": {
"FormatArc": "name,role\r\nMika,admin\r\nNoah,viewer",
"json-2-csv": "name,role\nMika,admin\nNoah,viewer",
"Miller": "name,role\nMika,admin\nNoah,viewer",
"pandas json_normalize": "name,role\nMika,admin\nNoah,viewer"
}
},
{
"id": "C2",
"label": "Nested object value",
"input": "[{\"name\":\"Mika\",\"address\":{\"city\":\"Tokyo\",\"zip\":\"150-0001\"}},{\"name\":\"Noah\",\"address\":{\"city\":\"Osaka\",\"zip\":\"530-0001\"}}]",
"outputs": {
"FormatArc": "name,address.city,address.zip\r\nMika,Tokyo,150-0001\r\nNoah,Osaka,530-0001",
"json-2-csv": "name,address.city,address.zip\nMika,Tokyo,150-0001\nNoah,Osaka,530-0001",
"Miller": "name,address.city,address.zip\nMika,Tokyo,150-0001\nNoah,Osaka,530-0001",
"pandas json_normalize": "name,address.city,address.zip\nMika,Tokyo,150-0001\nNoah,Osaka,530-0001"
}
},
{
"id": "C3",
"label": "Array of scalars as a value",
"input": "[{\"name\":\"Mika\",\"tags\":[\"admin\",\"billing\"]},{\"name\":\"Noah\",\"tags\":[\"viewer\"]}]",
"outputs": {
"FormatArc": "name,tags\r\nMika,\"[\"\"admin\"\",\"\"billing\"\"]\"\r\nNoah,\"[\"\"viewer\"\"]\"",
"json-2-csv": "name,tags\nMika,\"[\"\"admin\"\",\"\"billing\"\"]\"\nNoah,\"[\"\"viewer\"\"]\"",
"Miller": "name,tags.1,tags.2\nMika,admin,billing\nNoah,viewer,",
"pandas json_normalize": "name,tags\nMika,\"['admin', 'billing']\"\nNoah,['viewer']"
}
},
{
"id": "C4",
"label": "Array of objects as a value",
"input": "[{\"order\":\"A-1\",\"items\":[{\"sku\":\"X1\",\"qty\":2},{\"sku\":\"X2\",\"qty\":1}]},{\"order\":\"A-2\",\"items\":[{\"sku\":\"X3\",\"qty\":5}]}]",
"outputs": {
"FormatArc": "order,items\r\nA-1,\"[{\"\"sku\"\":\"\"X1\"\",\"\"qty\"\":2},{\"\"sku\"\":\"\"X2\"\",\"\"qty\"\":1}]\"\r\nA-2,\"[{\"\"sku\"\":\"\"X3\"\",\"\"qty\"\":5}]\"",
"json-2-csv": "order,items\nA-1,\"[{\"\"sku\"\":\"\"X1\"\",\"\"qty\"\":2},{\"\"sku\"\":\"\"X2\"\",\"\"qty\"\":1}]\"\nA-2,\"[{\"\"sku\"\":\"\"X3\"\",\"\"qty\"\":5}]\"",
"Miller": "order,items.1.sku,items.1.qty,items.2.sku,items.2.qty\nA-1,X1,2,X2,1\nA-2,X3,5,,",
"pandas json_normalize": "order,items\nA-1,\"[{'sku': 'X1', 'qty': 2}, {'sku': 'X2', 'qty': 1}]\"\nA-2,\"[{'sku': 'X3', 'qty': 5}]\""
}
},
{
"id": "C5",
"label": "Keys present on some records only",
"input": "[{\"id\":1,\"name\":\"Mika\"},{\"id\":2,\"name\":\"Noah\",\"nickname\":\"No\"},{\"id\":3,\"phone\":\"03-0000-0000\"}]",
"outputs": {
"FormatArc": "id,name,nickname,phone\r\n1,Mika,,\r\n2,Noah,No,\r\n3,,,03-0000-0000",
"json-2-csv": "id,name,nickname,phone\n1,Mika,undefined,undefined\n2,Noah,No,undefined\n3,undefined,undefined,03-0000-0000",
"Miller": "ERROR: mlr: CSV schema change: first keys \"id,name\"; current keys \"id,phone\"",
"pandas json_normalize": "id,name,nickname,phone\n1,Mika,,\n2,Noah,No,\n3,,,03-0000-0000"
}
},
{
"id": "C6",
"label": "null value",
"input": "[{\"id\":1,\"deleted_at\":null},{\"id\":2,\"deleted_at\":\"2026-07-01\"}]",
"outputs": {
"FormatArc": "id,deleted_at\r\n1,\r\n2,2026-07-01",
"json-2-csv": "id,deleted_at\n1,null\n2,2026-07-01",
"Miller": "id,deleted_at\n1,null\n2,2026-07-01",
"pandas json_normalize": "id,deleted_at\n1,\n2,2026-07-01"
}
},
{
"id": "C7",
"label": "Empty object value",
"input": "[{\"id\":1,\"meta\":{}},{\"id\":2,\"meta\":{\"source\":\"api\"}}]",
"outputs": {
"FormatArc": "id,meta,meta.source\r\n1,,\r\n2,,api",
"json-2-csv": "id,meta,meta.source\n1,{},undefined\n2,\"{\"\"source\"\":\"\"api\"\"}\",api",
"Miller": "ERROR: mlr: CSV schema change: first keys \"id,meta\"; current keys \"id,meta.source\"",
"pandas json_normalize": "id,meta.source\n1,\n2,api"
}
},
{
"id": "C8",
"label": "Three levels of nesting",
"input": "[{\"id\":1,\"meta\":{\"created\":{\"by\":{\"name\":\"Mika\"},\"at\":\"2026-07-01\"}}}]",
"outputs": {
"FormatArc": "id,meta.created.by.name,meta.created.at\r\n1,Mika,2026-07-01",
"json-2-csv": "id,meta.created.by.name,meta.created.at\n1,Mika,2026-07-01",
"Miller": "id,meta.created.by.name,meta.created.at\n1,Mika,2026-07-01",
"pandas json_normalize": "id,meta.created.by.name,meta.created.at\n1,Mika,2026-07-01"
}
},
{
"id": "C9",
"label": "Single top-level object (not an array)",
"input": "{\"name\":\"Mika\",\"address\":{\"city\":\"Tokyo\"}}",
"outputs": {
"FormatArc": "name,address.city\r\nMika,Tokyo",
"json-2-csv": "name,address.city\nMika,Tokyo",
"Miller": "name,address.city\nMika,Tokyo",
"pandas json_normalize": "name,address.city\nMika,Tokyo"
}
},
{
"id": "C10",
"label": "Values containing comma, double quote and newline",
"input": "[{\"who\":\"Smith, John\",\"quote\":\"She said \\\"hi\\\"\",\"note\":\"line1\\nline2\"}]",
"outputs": {
"FormatArc": "who,quote,note\r\n\"Smith, John\",\"She said \"\"hi\"\"\",\"line1\nline2\"",
"json-2-csv": "who,quote,note\n\"Smith, John\",\"She said \"\"hi\"\"\",\"line1\nline2\"",
"Miller": "who,quote,note\n\"Smith, John\",\"She said \"\"hi\"\"\",\"line1\nline2\"",
"pandas json_normalize": "who,quote,note\n\"Smith, John\",\"She said \"\"hi\"\"\",\"line1\nline2\""
}
},
{
"id": "C11",
"label": "Top-level array of primitives",
"input": "[\"a\",\"b\",\"c\"]",
"outputs": {
"FormatArc": "ERROR: Every array element must be an object (for example: [{...}, {...}]).",
"json-2-csv": "\n\n\n",
"Miller": "ERROR: mlr: valid but unmillerable JSON. Expected map (JSON object); got string",
"pandas json_normalize": "ERROR: TypeError: All items in data must be of type dict or NA-like, found str"
}
},
{
"id": "C12",
"label": "Empty array",
"input": "[]",
"outputs": {
"FormatArc": "ERROR: The JSON array is empty.",
"json-2-csv": "\n",
"Miller": "",
"pandas json_normalize": ""
}
},
{
"id": "C13",
"label": "A literal dot in a key name collides with a flattened path",
"input": "[{\"a.b\":1,\"a\":{\"b\":2}}]",
"outputs": {
"FormatArc": "a.b\r\n2",
"json-2-csv": "a\\.b,a.b\n1,2",
"Miller": "a.b\n2",
"pandas json_normalize": "a.b\n2"
}
},
{
"id": "C14",
"label": "Integers larger than 2^53",
"input": "[{\"id\":9007199254740993,\"order_no\":12345678901234567890}]",
"outputs": {
"FormatArc": "id,order_no\r\n9007199254740992,12345678901234567000",
"json-2-csv": "id,order_no\n9007199254740992,12345678901234567000",
"Miller": "id,order_no\n9007199254740993,12345678901234567890",
"pandas json_normalize": "id,order_no\n9007199254740993,12345678901234567890"
}
},
{
"id": "C15",
"label": "Values starting with = (spreadsheet formula characters)",
"input": "[{\"name\":\"=1+1\",\"note\":\"+41 44 000 00 00\"}]",
"outputs": {
"FormatArc": "name,note\r\n=1+1,+41 44 000 00 00",
"json-2-csv": "name,note\n=1+1,+41 44 000 00 00",
"Miller": "name,note\n=1+1,+41 44 000 00 00",
"pandas json_normalize": "name,note\n=1+1,+41 44 000 00 00"
}
}
]
}