json-to-csv-guide

このページは FormatArc のブログ記事が引用している実測の生データです。検索結果には表示されません (noindex)。

Files

FileSize
README.md3.7 KB
cases.json2.4 KB
measure.ts4.9 KB
package-lock.json2.4 KB
package.json341 B
results.json8.0 KB

README.md

# 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.

results.json

{
  "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"
      }
    }
  ]
}