Convert JSON to CSV with jq: Headers, Nested Fields and Quoting
To convert a JSON array of objects to CSV with jq, write the header and each row as arrays, pass them through @csv, and run jq -r to remove JSON's outer string quoting. Select columns explicitly so the order stays predictable even when records have different keys.
Export selected fields with a header
Save this array as input.json:
[
{"name":"Ada, Jr.","email":"ada@example.com","profile":{"city":"London"},"note":"Said \"hello\"","active":true},
{"name":"Lin","email":null,"profile":{"city":"Oslo"},"note":null,"active":false},
{"name":"Sam","profile":{},"active":false}
]
Run:
jq -r '(["name","email","city","note","active"], (.[] | [.name, .email, .profile.city, .note, .active])) | @csv' input.json > users.csv
users.csv contains:
"name","email","city","note","active"
"Ada, Jr.","ada@example.com","London","Said ""hello""",true
"Lin",,"Oslo",,false
"Sam",,,,false
Try the filter with this input in the jq playground
The first array is the header; .[] visits each record; the second array fixes the column order and reads the nested city. @csv adds quotes around text and doubles embedded quotes, so commas and quotes stay inside their cells. Missing fields and explicit null both become empty CSV cells; CSV cannot distinguish them without a deliberate sentinel. Boolean values remain true and false.
The browser playground runs the filter but displays its CSV lines as JSON-quoted strings. -r and > users.csv are shell features: use a terminal to create the raw CSV file. Without -r, jq emits JSON strings such as "\"Ada, Jr.\"", not directly usable CSV rows. The browser's JSON to CSV converter is a simpler choice when you do not need jq's field selection.
One CSV row per nested order
For a specific nested-array task, use a second input shape:
{"users":[{"id":"u1","orders":[{"id":"a","total":12.5},{"id":"b","total":9}]},{"id":"u2","orders":[]}]}
jq -r '(["user_id","order_id","total"], (.users[] | . as $user | .orders[]? | [$user.id, .id, .total])) | @csv' input.json
Output:
"user_id","order_id","total"
"u1","a",12.5
"u1","b",9
Try the nested-order filter in the jq playground
Here . as $user retains the parent ID while .orders[]? emits one row for each order. A user with no orders produces no row; this is not a general-purpose flattening rule for every nested array. If you need one row per user instead, decide how to summarize or serialize that user's orders first.
Before using the CSV
- Select a known set of columns instead of relying on the first object's keys; later records may add fields.
- Avoid
// ""for boolean columns: jq treatsfalseas a fallback value and would turn it into an empty cell. - Check how your spreadsheet imports unquoted numbers and booleans. CSV is text, so types and the distinction between
nulland missing values are not retained.
Related: extract multiple fields with jq, handle null values and defaults, and the jq cheatsheet. See the jq manual's format strings for @csv details.