Convert JSON to CSV with jq: Headers, Nested Fields and Quoting

All jq tools and guides

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 treats false as 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 null and 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.