JSONはツリーで、CSVは長方形の表です。一方から他方へ変換するには、ツリーをどう平らにするかを決める必要があります。どの変換ツールもその判断を代わりに行いますが、何を決めたのかは言わないことがほとんどです。同じファイルを3つのオンライン変換ツールに通すと、3通りの表ができることがあるのはそのためです。
3つの判断を知っておけば、思いどおりに判断してくれるツールや設定を選べます。
先に結論
- オブジェクトのフラットな配列(
[{"id":1,"name":"Ada"}, …]):どの変換ツールでも問題ありません。jq、Python、DuckDB、Excel、アプリのどれでも使えます。 - 入れ子のオブジェクト(
"address": {"city": "Leeds"}):address.cityのようにドット区切りの列へ展開します。Pythonのjson_normalizeなら1行です。 - 各レコードの中にある配列(
"tags": ["a","b"]):要素ごとに列を作る、要素ごとに行を作る、1つのセルに連結する、のどれかを選びます。正解はなく、変換後の使い道に合うものが正解です。 - レコードが最上位にない場合: まず配列を探し(たいてい
data、items、results)、その配列を変換します。
判断1:どの配列を行にするか
CSVには、似たもののリストが1行に1件ずつ必要です。実際のJSONで最初からそうなっているものはほとんどありません。APIのレスポンスは次のような形です。
{
"status": "ok",
"page": 1,
"data": {
"items": [
{ "id": 1, "name": "Ada", "address": { "city": "Leeds" }, "tags": ["admin"] },
{ "id": 2, "name": "Lin", "address": { "city": "Hue" }, "tags": [] }
]
}
}
行になるのは data.items です。ルートを指定した変換ツールはオブジェクトが1つだけと見なし、status、page、そしてエスケープされたJSONが詰まった列を持つ1行を返します。「変換ツールが壊れている」という苦情で最も多いのがこれで、原因は変換ツールではなくパスの指定です。
判断2:入れ子のオブジェクトをどうするか
address は各レコードの中にあるオブジェクトです。たいていの用途では、ドット区切りの列名に展開するのが普通で、正しい選択です。
| id | name | address.city |
|---|---|---|
| 1 | Ada | Leeds |
| 2 | Lin | Hue |
もう1つの方法は、オブジェクト全体をJSON文字列として1つのセルに入れることです。情報はすべて残りますが、並べ替えもフィルタもできない列になります。
判断3:レコード内の配列をどうするか
tags はリストで、リストは1つのセルには収まりません。方法は3つあります。
| 方法 | 結果 | 向いている用途 |
|---|---|---|
| インデックス列 | tags[0]、tags[1]、… | 最大数が分かっている短いリスト |
| 行に展開 | タグごとに1行、他の列は繰り返し | タグでの集計やフィルタ |
| 1つのセルに連結 | admin;editor | 読むだけで分析しない場合 |
展開すると行数が変わります。タグが3つある人は3行になります。「各タグを持つ人は何人か」を調べるなら正しく、「全部で何人いるか」を調べるなら間違いです。選ぶ前に、そのCSVでどんな問いに答えたいのかを決めておきましょう。
そのほかに失われるもの
完璧に平らにしても、CSVでは表現できないものが失われます。
- 型。
"42"も42も42になります。trueは文字列のtrueになります。 - nullと空。
null、""、キーがない場合は、どれも空のセルになります。 - まばらなレコードの列。 10,000件のうち1件だけに
couponフィールドがある場合、最初のレコードのキーからヘッダーを作る変換ツールはその列を落とします。良い変換ツールは、全レコードのキーの和集合を使います。
jqの場合
最上位がフラットな配列のとき:
jq -r '(.[0] | keys_unsorted) as $k | $k, (.[] | [.[$k[]]]) | @csv' in.json > out.csv
これは最初のレコードからヘッダーを作るので、最初のレコードにないキーは落ちます。上で触れた「まばらなレコード」の問題です。配列がもっと深い場所にある場合は、最初の . を .data.items のようなパスに置き換えます。入れ子のデータもjqで処理できますが、式が読めなくなってきます。そのときはPythonを使いましょう。
Pythonの場合
pandas.json_normalize は3つの判断を1回の呼び出しで行い、キーの和集合も取ってくれます。
import json, pandas as pd
data = json.load(open("in.json"))
df = pd.json_normalize(data["data"]["items"], sep=".")
df.to_csv("out.csv", index=False)
入れ子のオブジェクトは address.city になります。リストは1つのセルの中にリストのまま残ります。行に展開したいときは、保存の前に df = df.explode("tags") を加えます。
DuckDBの場合
大きなファイルなら、DuckDBはJSONを直接読み込み、全体をメモリに載せずにCSVを書き出せます。
duckdb -c "COPY (SELECT * FROM read_json_auto('in.json')) TO 'out.csv'"
フラットな配列なら、これが最速の方法です。入れ子のオブジェクトはDuckDB独自のstruct表記で1つの列に出力されます。SELECT の中で展開するか、入れ子のデータはPythonを使ってください。
Excelの場合
データ → データの取得 → ファイルから → JSONから でPower Queryが開きます。テーブルへ をクリックし、列見出しの展開アイコン(矢印が2つ)で、レコードやリストを1階層ずつ展開します。クリックだけで操作でき、判断の結果をその場で確認できます。そのあとCSVとして保存します。書き出し時の型変換には注意してください。指定しない限り、ExcelはCSVのデータを書き換えます。
オンライン変換ツール
使えますし、公開データなら問題ありません。注意点は2つです。多くのツールは3つの判断を見せないので、出力の行数と列数が想定と合っているか確認してください。また、APIのJSONにはトークン、メールアドレス、顧客データが含まれることがよくあります。アップロードせずブラウザ内で動く変換ツールを選びましょう。
iPhoneとiPadの場合
Smart Jsonは「レコードモード」と呼ぶ方式でJSONをCSVに変換します。オブジェクトの配列が行になり、入れ子の値は address.city、tags[0] のようなドット区切りの列に展開され、ヘッダーは全レコードのキーの和集合になります。最初のオブジェクト配列は自動で見つけ、別の配列を選ぶことも、$.data.items のようなJSONPathを入力することもできます。区切り文字は変更できます。オブジェクトの配列がないドキュメントでは、役に立たない2列のダンプを出す代わりに、その旨を表示します。

プレビューは無料なので、CSVがどう仕上がるかを正確に確認できます。結果のコピーと保存はProの機能です。
JSONをCSVとしてプレビューし、入れ子の値をドット区切りの列に展開します。レコードの配列はJSONPathで選べ、CSVで保持できないものもはっきり表示します。
できることCSVにできたら、スマートフォンで開くことが次の小さな問題になります。そちらのガイドで説明しています。
結果を確認する
どのツールを使う場合でも、確認は3つです。
- 行数。 配列のレコード数と一致していますか(リストをあえて展開したなら、それより多くなります)。
- 列数。 一部のレコードにしかないフィールドが、ヘッダーに入っていますか。
- 入れ子の値を1つ。 入れ子のオブジェクトを持つレコードを選び、その値が正しいドット区切りの列に入っているか確かめます。