FormatArc JSON 轉 CSV 工具。左側為含巢狀 address 的 JSON 陣列,右側為含 address.city 欄位的 CSV 結果FormatArc JSON 轉 CSV 工具。左側為含巢狀 address 的 JSON 陣列,右側為含 address.city 欄位的 CSV 結果
作者: FormatArc 編輯部發布日期: 2026-09-02更新日期: 2026-09-02

JSON 轉 CSV 比較 — 巢狀物件、陣列與 null 的 4 款工具實測

TL;DR — 按用途 10 秒選工具

  • 現在就要轉成表格:FormatArc JSON to CSV。瀏覽器內完成,不上傳資料,巢狀物件展開為點記法欄位。
  • 一筆記錄裡面有物件陣列(訂單與明細):先決定「一行 = 一筆訂單」還是「一行 = 一筆明細」。參考下方「各種 JSON 形狀的處理方式」。
  • 陣列每個元素要各占一行:用 mlr --ijson --ocsv cat (Miller) 或 pandas.json_normalize(data, record_path=...)
  • ID 超過 15 位:不要用 JavaScript 系的轉換器。參考「超過 2^53 的整數」。
  • 之後要在 Excel 打開:雙擊之前先讀「在試算表中不破壞資料地開啟」。
方法安裝巢狀物件記錄內的陣列瀏覽器完成(不上傳)
FormatArc不需要點記法欄位以 JSON 文字留在一個儲存格
json-2-csv (npm)npm i json-2-csv點記法欄位以 JSON 文字留在一個儲存格否(Node)
Miller (mlr)brew install miller點記法欄位展開為 items.1.skuitems.2.sku否(CLI)
pandas json_normalizepip install pandas點記法欄位Python repr 放一個儲存格,或用 record_path 展開成行否(Python)
jqbrew install jq自己寫對應自己寫對應否(CLI)

上表內容全部是實測值。輸入資料、量測腳本、原始輸出都放在倉庫的 scripts/benchmarks/json-to-csv-guide/(2026-07-26 量測),下面引用的輸出都是那次執行的結果。

30 秒完成轉換

打開 JSON to CSV,貼上物件陣列,按執行。以下輸入:

[
  { "name": "Mika", "email": "mika@example.com", "role": "admin", "address": { "city": "Tokyo" } },
  { "name": "Noah", "email": "noah@example.com", "role": "viewer", "address": { "city": "Osaka" } }
]

產出的 CSV:

name,email,role,address.city
Mika,mika@example.com,admin,Tokyo
Noah,noah@example.com,viewer,Osaka

FormatArc JSON 轉 CSV 將含巢狀 address 的 JSON 陣列轉換為含 address.city 欄位的 CSVFormatArc JSON 轉 CSV 將含巢狀 address 的 JSON 陣列轉換為含 address.city 欄位的 CSV

轉換完全在瀏覽器頁面上完成。JSON 資料不會傳送到任何伺服器。當你要貼的是產線 API 的回應、裡面有客戶資料時,這一點就很重要了。哪些被保護、哪些不被保護,見線上轉換工具是否安全

輸入不是合法 JSON 的話,會跟 JSON 整理器一樣回傳帶行號的錯誤訊息。常見原因與修法見JSON 解析錯誤的原因與修法

同一組 JSON 跑 4 款轉換器,結果各不一樣

JSON 是樹狀結構,CSV 是矩形表格。每款轉換器都有自己的規則來把樹壓扁成矩形,而這些規則互不相同。我們用 15 筆輸入跑過 4 款轉換器,逐一記錄輸出。

執行環境(取自 results.json):macOS arm64 上的 Node v26.3.1、FormatArc 用 lib/tooling.ts(PapaParse 5.5.2)、json-2-csv 5.5.11、Miller 6.19.0、pandas 3.0.5。四款都是預設設定,沒有任何選項。目的就是看「什麼都沒設定的時候會怎樣」。

四款一致的部分

巢狀物件會被展開成點記法。對 {"name":"Mika","address":{"city":"Tokyo","zip":"150-0001"}},四款都輸出:

name,address.city,address.zip
Mika,Tokyo,150-0001

三層巢狀(meta.created.by.name)也一樣。未被陣列包起來的單一物件,四款都變成一行 CSV。含逗號、雙引號、換行值的引號處理也一致:

who,quote,note
"Smith, John","She said ""hi""","line1
line2"

這跟 RFC 4180在新分頁中開啟 第 2 節的引號規則完全吻合:含換行、雙引號、逗號的欄位用雙引號包起來,內部的雙引號用兩個來轉義。補一點方便比對檔案的人:記錄分隔符 FormatArc 用 CRLF(PapaParse 的預設值,也是 RFC 4180 規定的),json-2-csv、Miller、pandas 用 LF。

陣列分三種處理

輸入:

[{"name":"Mika","tags":["admin","billing"]},{"name":"Noah","tags":["viewer"]}]
轉換器輸出
FormatArcname,tags / Mika,"[""admin"",""billing""]" / Noah,"[""viewer""]"
json-2-csv與 FormatArc 相同
Millername,tags.1,tags.2 / Mika,admin,billing / Noah,viewer,
pandasname,tags / Mika,"['admin', 'billing']" / Noah,['viewer']

三種完全不同的產物。FormatArc 和 json-2-csv 把陣列當「合法 JSON 文字」放在一個儲存格裡,之後可以逐格再解析。Miller 把表格拉寬——很方便,但如果某筆記錄有 40 個 tag,欄位數就會爆到 40。pandas 輸出的是帶單引號的 Python repr,這不是合法 JSON,後段如果 JSON.parse 那個儲存格就會失敗。

物件陣列也一樣。{"order":"A-1","items":[{"sku":"X1","qty":2},{"sku":"X2","qty":1}]} 在 FormatArc 和 json-2-csv 裡變成一個儲存格的 JSON 文字,在 Miller 裡變成 items.1.sku, items.1.qty, items.2.sku, items.2.qty

缺鍵:空儲存格、"undefined" 字串、還是報錯

實際 API 回應裡常有選填欄位。輸入:

[{"id":1,"name":"Mika"},{"id":2,"name":"Noah","nickname":"No"},{"id":3,"phone":"02-1234-5678"}]

FormatArc 和 pandas 輸出空儲存格:

id,name,nickname,phone
1,Mika,,
2,Noah,No,
3,,,02-1234-5678

json-2-csv 在缺失處寫 undefined 這個字串:

id,name,nickname,phone
1,Mika,undefined,undefined
2,Noah,No,undefined
3,undefined,undefined,02-1234-5678

Miller 直接拒絕整個檔案:mlr: CSV schema change: first keys "id,name"; current keys "id,phone"。對串流處理工具來說這是合理的設計,但意思是說多了一個選填欄位,昨天還跑得通的管線就停了。

FormatArc 會收集所有記錄的鍵之並集(按首次出現順序),在寫第一行之前就確定欄位數。行不會跑偏。

null:空儲存格還是 n-u-l-l 四個字

[{"id":1,"deleted_at":null},{"id":2,"deleted_at":"2026-07-01"}]

FormatArc 和 pandas 輸出空儲存格,json-2-csv 和 Miller 寫入 null 四個字。把這個 CSV 直接匯入資料庫的話,前者是真正的 NULL,後者是一個長得像 NULL 的字串。從 JSON 轉 CSV 之後「WHERE 條件對不上」的問題,大部分都出在這裡。

空物件會產生四種不同的表格

[{"id":1,"meta":{}},{"id":2","meta":{"source":"api"}}]

轉換器輸出
FormatArcid,meta,meta.source — 因為空的 {},所有行都留一個空的 meta
json-2-csv表頭相同,但把 {}{"source":"api"} 原樣寫進儲存格,值重疊
Miller報錯:CSV schema change: first keys "id,meta"; current keys "id,meta.source"
pandasmeta 欄整個消失:id,meta.source

FormatArc 的行為誠實但不美觀。多出一個空欄的話,就是 payload 某個地方藏著空物件 {}

鍵名含逗點時,三款工具會覆寫欄位

這是會丟資料的情況,四款裡面三款會丟。輸入:

[{"a.b":1,"a":{"b":2}}]

這筆記錄有兩個不同的值:字面上叫 a.b 的鍵的值 1,以及巢狀 ab 路徑的值 2。FormatArc、Miller、pandas 都輸出:

a.b
2

1 不見了。只有 json-2-csv 分得出來,對字面鍵做轉義:

a\.b,a.b
1,2

如果 JSON 用的是含逗點的鍵名(分析事件名 page.view.count、MongoDB 文件、Prometheus 標籤),轉之前先確認有沒有衝突,或用 json-2-csv 來轉。

超過 2^53 的整數在 JavaScript 系會被四捨五入

[{"id":9007199254740993,"order_no":12345678901234567890}]
轉換器輸出
FormatArc9007199254740992,12345678901234567000
json-2-csv9007199254740992,12345678901234567000
Miller9007199254740993,12345678901234567890
pandas9007199254740993,12345678901234567890

這不是 CSV 的問題,也不是那兩款 JavaScript 工具的 bug。JSON.parse 把所有數字轉成雙精確度浮點數,所以 2^53 以上的整數無法精確表示。值在傳到 CSV 寫入器之前就已經變了。Snowflake ID、X(Twitter)ID、某些付款序號、64-bit 資料庫主鍵都落在這個範圍。要處理長 ID 的話,在 JSON 端用字串包起來,或用保留位數的 Miller、pandas、jq

爛輸入:明確報錯還是靜默空檔案

輸入FormatArcjson-2-csvMillerpandas
["a","b","c"]錯誤:「陣列的每個元素必須是物件」三行空白、無錯誤例外例外
[]錯誤:「JSON 陣列為空」一行空白、無錯誤空輸出、無錯誤空輸出、無錯誤

三種結果裡最糟的是「回傳碼 0 但輸出空檔案」。cron 批次工作會毫不猶豫地用空檔覆蓋昨天的正常檔案。

FormatArc 的轉換規則(實作原樣)

以下是摘要不是重點整理,是 lib/tooling.tsconvertJsonToCsv 函式的實際規則,跟上面的實測結果完全一致。

  1. 最上層必須是物件陣列或單一物件。單一物件變成一行 CSV。原始型別陣列、空陣列、裸字串或數字都會被拒絕並附說明。
  2. 巢狀物件遞迴展開為點記法欄位(address.citymeta.created.by.name)。沒有深度限制。
  3. 陣列不展開。用 JSON.stringify 序列化後放在一個儲存格,所以 ["admin","billing"] 保持可解析狀態。
  4. 欄位是所有記錄鍵的並集,按首次出現順序排列。只在最後一筆記錄出現的鍵也會有欄位,之前的行填空儲存格。行不會跑偏。
  5. nullundefined 變空儲存格。空物件 {} 也變空儲存格,但保留自己的欄位。
  6. 引號處理跟隨 PapaParse 的 unparse:含逗號、引號、換行的欄位加引號,內部引號翻倍,記錄分隔為 CRLF。
  7. 不合法的 JSON 會帶行號報錯(跟 JSON 整理器同一條錯誤路徑)。

資料不會離開瀏覽器分頁。沒有上傳步驟,伺服器上沒有暫存檔,就是在已載入的頁面裡執行一個函式。

各種 JSON 形狀:怎麼轉

扁平物件陣列

沒有要判斷的。貼上、執行,完事。

含巢狀物件的記錄

點記法是各工具都採用的預設答案,人眼看也追得到來源(address.city 從哪來一目了然)。事先確認兩件事就好:已有的鍵名裡有沒有逗點(前面提到的衝突),以及讀取 CSV 的程式能不能處理表頭裡的逗點。有些 SQL loader 需要表頭加引號,Google 試算表則沒問題。

含標量陣列的記錄

三個選項:

  • 保持 JSON 文字放在一個儲存格(FormatArc 預設)。適合 CSV 當中間檔案、後段腳本要再讀的情況。
  • 用 Miller 展開成 tags.1tags.2 欄位。適合元素數量少且固定(座標對、RGB 三個值)的情況。
  • 給人看的欄位就在轉換前用分隔符號串起來:jq '.[] |= (.tags |= join(";"))' data.json 跑完再轉。分隔用 ; 而非逗號,儲存格就不用加引號了。

含物件陣列的記錄(最棘手的形狀)

在碰轉換器之前,先定義「CSV 的一行代表什麼」。

  • 一行 = 父層(一筆訂單):陣列保持 JSON 文字放在一個儲存格。FormatArc 預設就是這樣。明細不會壞,之後可以用腳本解析那個儲存格。
  • 一行 = 子層(一筆明細):這是另一張表,不是同一張表的另一種格式。用 pandas.json_normalize(data, record_path="items", meta=["order"]) 可以把父層欄位重複展開到每個子行。
  • 拆成兩個檔案:父層欄位轉成 orders.csv,子層用 jq '[.[] | .order as $o | .items[] | {order:$o} + .]' data.json 抽出再轉成 items.csv。這是正規化的答案,CSV 要進資料庫的話選這個。

要避免的是:用 Miller 預設行為把可變長陣列拉成 items.1.sku, items.2.sku, …。欄位數由「剛好子元素最多的那筆記錄」決定,資料一變 schema 就變。

鍵不齊整的記錄

FormatArc 和 pandas 不用設定就能處理。Miller 加 unsparsify 就不會中斷、會補上缺失值:

mlr --ijson --ocsv unsparsify data.json

或者用 jq '[.[] | {id, name, nickname, phone}]' 先讓所有記錄都帶齊所有鍵。

在 Excel 與 Google 試算表中不破壞資料地開啟

破壞資料的通常不是轉換器,是試算表軟體本身。

  • 編碼。CSV 格式本身沒有編碼欄位。RFC 4180在新分頁中開啟 也只提到 US-ASCII 以外的字元集用 MIME charset 參數表示,本機檔案沒有這個參數。FormatArc 下載的是沒有 BOM 的 UTF-8。Windows 的 Excel 沒有 BOM 時可能把非 ASCII 文字當成 Windows-1252 讀,字就亂了。不要雙擊檔案,用 Excel 的「資料」分頁 >「從文字/CSV」,把編碼選成「65001: Unicode (UTF-8)」再匯入。Google 試算表會自動正確判讀 UTF-8。
  • 15 位以上的數字。Excel 的官方規格寫明數字精度為 15 位在新分頁中開啟,超過 15 位的數字會被替換為 0。加上前面的 2^53 四捨五入,長 ID 可能兩次受傷。該欄位匯入時要指定為文字型別。
  • 前導零。007 或電話 02-1234-5678,如果不把欄位當文字匯入,就會變成數字 7 或日期。
  • 看起來像日期的字串。2026-07-01 當然會,1-2 這種版本號也會被試算表自動轉成日期格式。匯入時指定欄位型別為文字最安全。
  • =+-@ 開頭的儲存格。這次比對的四款工具都沒有轉義(=1+1 原樣輸出)。要不要當成公式來算是試算表決定的,這就是 CSV Injection(公式注入攻擊)在新分頁中開啟 的基本型。如果 CSV 含使用者輸入且會有別人打開,在前頭加單引號(')或把該欄匯入為文字。

分隔符號:逗號、分號、還是 Tab

FormatArc 永遠用逗號(,)。輸出經過 PapaParse 的 unparse 函式,沒有分隔符號設定選項(lib/tooling.tsconvertJsonToCsv)。RFC 4180在新分頁中開啟 也把分隔符定義為逗號。

這個預設值會一直撐到有人雙擊檔案為止。Microsoft 的文件說 Excel 把活頁簿存成 .csv 時「預設清單分隔符號是逗號,可以用 Windows 區域及語言設定改成其他字元」(匯入或匯出文字檔案在新分頁中開啟)。同一頁還寫到:小數點符號用逗號的地區,「Excel 會改用分號作為清單分隔符號」。歐洲大陸和大部分中南美洲都是這個設定。在那種環境下雙擊逗號分隔的檔案,所有內容可能全擠在 A 欄。

兩個有效解法,一個無效:

  • 不要雙擊,用匯入功能。「資料」>「從文字/CSV」,分隔符號選逗號、編碼選 UTF-8。跟上面編碼那條同一條路徑,不會多步驟。
  • 如果一定要讓對方雙擊打開,用 Miller 產分號分隔的檔案。上面比較用的 Miller 6.19.0 實測,mlr --ijson --ocsv --ofs ';' cat data.json 會把表頭 a,b 變成 a;b
  • 不要把逗號批量替換成分號。原本含逗號的欄位已經被引號包住了,替換會把那個欄位從中間切開,資料就壞了。

瀏覽器不適合的時候

curl 抓到的 API 回應貼上去確認,瀏覽器轉換器最快也最安全。但 2GB 的匯出檔、或夜間批次工作的一個步驟,瀏覽器就不適合了。

# Miller: 物件展開為點記法、陣列展開為索引欄位、串流處理
mlr --ijson --ocsv cat data.json > out.csv

# jq: 自己寫對應,沒有猜測空間
jq -r '(.[0] | keys_unsorted), (.[] | [.[]]) | @csv' data.json > out.csv

# pandas: 點記法展開,加上 record_path 可逐元素展開為行
python3 -c "import pandas,json; print(pandas.json_normalize(json.load(open('data.json'))).to_csv(index=False))" > out.csv

這三款不能互相替換。json_normalize 把字典展開成點記法欄位,但要逐元素出行的話必須指定 record_pathjq@csv 需要輸入已經是標量陣列,巢狀物件傳進去不會展開、會直接報錯。Miller 預設會展開物件和陣列(陣列索引從 1 開始),但記錄 schema 不一致時會中斷(前面實測過)。詳細見 Miller flatten 文件在新分頁中開啟jq 手冊在新分頁中開啟pandas.json_normalize 參考在新分頁中開啟

從 CSV 轉回 JSON 回不到原狀

往返回不去。拿第一個例子的 CSV 再用 CSV to JSON 工具轉回來:

[
  {
    "name": "Mika",
    "email": "mika@example.com",
    "role": "admin",
    "address.city": "Tokyo"
  }
]

address.city 回來的是一個含逗點的扁平鍵名,不是巢狀的 address 物件。沒有工具能自動把點記法重新巢狀化,因為 address.city 本身就是一個完全合法的 JSON 鍵名,轉換器無法判斷你原本的意思是哪個。跟前面鍵名衝突是同一個原因。

所以 JSON 轉 CSV 就當它是「試算表分析用的單向匯出」,正本永遠是 JSON。需要巢狀形狀回來的話,用 jq 'map(reduce (to_entries[]) as {$key,$value} ({}; setpath($key | split("."); $value)))' 這樣明確地重新巢狀化,然後檢查結果。反方向(CSV 轉 JSON)的比較見CSV 轉 JSON 比較指南

常見問題

為什麼 CSV 只有一欄,裡面是整塊 JSON?

最上層是 {"data":[...]} 這種「只含一個陣列的單一物件」。先把陣列抽出來(只貼 data 的值,或 jq '.data' response.json 跑一下),再轉。FormatArc 會把外層物件展開成 data 欄,不會把裡面的陣列自動當成行。

巢狀陣列的每個元素能各占一行嗎?

瀏覽器轉換器做不到,這是故意的。行數會隨資料變,父層欄位也會被無聲地複製。用 pandas.json_normalize(data, record_path="items", meta=[...]) 或先用 jq 整形,如前所述。

欄位內容是 [{"sku":"X1"...}] 這種形式

物件陣列被保持為 JSON 文字放在一個儲存格。這是預期行為,資料沒有丟,之後可以還原。想改的話有三種方式,見「含物件陣列的記錄」。

數字會變嗎?

只有在 JavaScript 自身規定的範圍內才會。超過 2^53 的整數在解析時會被四捨五入(前面實測過)。字串完全不動。ID 超過 15 位的話,在 JSON 端用字串包起來,或用非 JavaScript 的工具。

資料會上傳到哪裡嗎?

不會。轉換在頁面內執行,JSON 不會離開分頁,連承載它的請求都不存在。

JSON 不合法會怎樣?

不會輸出壞掉的 CSV,而是回傳帶行號和內容的錯誤訊息。常見原因(結尾逗號、單引號等)的處理方式可參考 JSON 整理器的說明。整形後維持可讀性的技巧見JSON 排版實務指南

正本該用哪個格式保存?

用能保存結構的那個。CSV 是沒有型別的矩形表格,巢狀、陣列、null 與空字串 "" 的區別全部消失。以 JSON 為正本,CSV 當其中一個檢視(view)。CSV 的基本結構與限制見CSV 是什麼?