jq Quick Reference
Slice, filter, and reshape JSON on the command line. Pipe curl into jq and stop eye-parsing giant payloads.
jq is a small language for JSON. A program is a pipeline of filters: each takes one JSON value in and produces zero or more values out. . is the current value, | feeds every output on its left into the filter on its right, and , emits several outputs. Most of jq is that rule plus builtins.
Wrap the program in single quotes so the shell leaves it alone, and use double quotes for strings inside it: curl -s $URL | jq '.items[] | select(.ok) | .id'. Rows show the filter exactly as typed between those quotes.
Access
- Whole thing, pretty
jq .- A field
jq .name· nested:jq .user.email- Array element
jq '.[0]'· last:jq '.[-1]'- All array elements
jq '.[]'— stream each item- Safe access
jq '.a?'— no error when the input isn’t an object; a missing key is alreadynull- .["content-type"]
- keys with dashes, dots or spaces need brackets (or
."content-type") - .[2:5]
- slice an array or string — end exclusive;
.[-2:]is the last two - ..
- recursive descent, every value at every depth:
[.. | .id? // empty]
Filter & transform
- Pick fields
jq '{name, id}'· rename:jq '{n: .name}'- Map an array
jq 'map(.price)'· orjq '.[].price'- Filter
jq '.[] | select(.age > 30)'- Count / length
jq 'length'·jq 'map(select(.ok)) | length'- Sort / unique
jq 'sort_by(.age)'·jq 'unique'- Keys
jq 'keys'· values:jq '[.[]]'- map(select(.ok))
- filter but keep an array —
.[] | select(…)streams separate values instead - any(.tags[]; . == "vip")
- array contains a value — use inside
select(…) - IN("paid", "open")
- equals one of several values:
select(.status | IN("paid", "open")) - test("^err"; "i")
- regex match → boolean; flags go in the second argument (
i= ignore case)
Aggregate & group
- add
- sum numbers, join strings, concat arrays, merge objects —
nullfor an empty array - map(.n) | add / length
- average — errors on an empty array (
nulldivided by 0), so guard it - group_by(.status)
- sorts by the key, then returns an array of arrays, one per distinct key
- reduce .[] as $x (0; . + $x.n)
- fold: a start value, then
.(the accumulator) updated once per item - unique_by(.id)
- one element per distinct key; output comes back sorted by that key
- min_by(.age) / max_by(.age)
- the whole element with the smallest / largest key, not just the number
- sort_by(.a, -.b)
- multi-key sort;
-flips numeric keys only —reversethe result for strings
Reshape objects
- {(.k): .v}
- computed key — parentheses turn the key into an expression
- to_entries / from_entries
{"a":1}↔[{"key":"a","value":1}]— the bridge for key work- with_entries(f)
- map over key/value pairs:
with_entries(select(.value != null))drops null fields - del(.password, .meta.tmp)
- remove keys;
del(.[] | select(.x))removes matching array items - . + {b: 2}
- shallow merge — the right side wins on clashing keys
- .a * .b
- deep merge of two objects; arrays are replaced, not concatenated
- pick(.a, .b.c)
- keep only these paths, nesting preserved (jq 1.7+)
Update & defaults
- .a = 1
- set a value — creates missing keys and intermediate objects
- .a |= . + 1
- update from the old value: on the right,
.is the current.a - .n += 1
- also
-=*=; a missing key isnull, andnull + 1is1 - .name // "anon"
- default when the left is
null,falseor produces nothing - if .a then 1 else 2 end
elifchains;elseis optional in jq 1.7+ (the input passes through)- try tonumber catch "bad"
- handle an error yourself; a postfix
?just drops it
Arguments & variables
- --arg name val
- binds
$name— always a string - --argjson n 5
- binds
$nparsed as JSON: numbers, booleans, arrays, objects - --slurpfile cfg f.json
$cfg= an array of every value in the file- --rawfile t notes.txt
$t= the whole file as one string- $ENV.HOME
- environment variables, no flag needed (also
env.HOME) - . as $root | …
- bind any value to reuse further down the pipeline
Strings & formats
- split(",") / join(",")
- string ↔ array;
jointurnsnullinto an empty string - sub(re; s) / gsub(re; s)
- replace the first / every regex match:
gsub("\\s+"; " ") - capture("(?<u>.+)@(?<h>.+)")
- named regex groups → an object
{"u": …, "h": …} - ltrimstr("v") / rtrimstr
- drop a fixed prefix / suffix if present;
startswith/endswithtest for it - tostring / tonumber
- convert;
tonumbererrors on non-numeric strings, so wrap it intry - fromjson / tojson
- parse JSON embedded in a string field / serialize a value to a string
- @base64 @base64d @uri @sh
- encode or decode a string;
@shquotes it safely for a shell command - todate / fromdate
- epoch seconds ↔ ISO 8601;
fromdaterejects fractional seconds
Output & input flags
- Raw strings (no quotes)
jq -r .name— pipe into shell safely- To CSV/TSV
jq -r '.[] | [.id, .name] | @csv'- Compact
jq -c .— one line, good for streaming- Build a string
jq -r '"\(.name): \(.age)"'- -s
- slurp: read every input into one array — NDJSON to an array
- -n
- null input: ignore stdin, or read it lazily with
input/inputs - -R / -Rs
- each line as a raw string / the whole input as one string
- -e
- exit 1 if the last output is
falseornull, 4 if none — for shellif - -S / --tab / --indent 4
- sort keys / indent with tabs / indent with n spaces
- -f prog.jq
- read the filter from a file — no shell quoting,
#comments allowed
Gotchas
- Cannot index array with "x"
- you used
.xon an array — use.[].xormap(.x) - --arg and numbers
.age > $nis silently false for a string — use--argjsonor($n | tonumber)- false // "x"
- gives
"x"://replacesfalseas well asnull - jq … f > f
- the shell truncates
fbefore jq reads it — write a temp file, thenmv - .a.b?
?covers only its own step —.a?.b?ortryfor the whole path- Big integer IDs
- arithmetic converts to doubles, so IDs past 2^53 lose digits; jq 1.7 keeps untouched literals exact
Recipes
Turn an API response into CSV with a header row
curl -s https://api.example.com/users \
| jq -r '(["id","name","email"] | @csv), (.[] | [.id, .name, .email] | @csv)' \
> users.csvThe comma runs both expressions, so the header prints first. @csv needs an array of scalars: a nested array or object in a cell is an error, so join(";") it first.
Count records per status, two ways
# sorted array of {status, count}
jq 'group_by(.status) | map({status: .[0].status, count: length})' orders.json
# one object in one pass: {"paid": 2, "open": 1}
jq 'reduce .[] as $o ({}; .[$o.status] += 1)' orders.jsongroup_by sorts first, so its output is ordered by status. The reduce version relies on null + 1 = 1 for each key’s first sighting.
Filter on values that come from the shell
min_age=18
jq --arg owner "$USER" --argjson min "$min_age" \
'[.[] | select(.owner == $owner and .age >= $min) | .name]' people.jsonNever paste a shell variable into the filter text: a quote in the value breaks the program. --arg always binds a string, so the number goes in with --argjson — with --arg the comparison silently matches nothing.
Edit a JSON file in place
jq '.version = "2.1.0" | .keywords += ["cli"] | del(.devDependencies.unused)' \
package.json > package.json.tmp && mv package.json.tmp package.jsonjq has no in-place flag, and jq … f > f empties the file before jq reads it. Write to a temp file and rename it (or pipe into sponge f from moreutils).
Convert between NDJSON and arrays, and merge files
jq -c '.[]' events.json > events.ndjson # array -> one object per line
jq -s '.' events.ndjson > events.json # lines -> one array
jq -s 'add' page1.json page2.json # concatenate two arrays
jq -s '.[0] * .[1]' base.json override.json # deep-merge two config objects-s loads every input into memory before the filter runs. For multi-gigabyte input use -n with reduce inputs as $x (…), which reads one value at a time.
Questions about jq Cheat Sheet
How do I pass a shell variable into a jq filter?
Use --arg name "$VAR", which binds $name inside the filter as a string, or --argjson name value when the value is a number, boolean, array or object. Do not paste the variable into the quoted program text: a value containing a quote breaks the program, and untrusted input could inject filter code. Because --arg always produces a string, numeric comparisons against it fail silently, since jq orders every number before every string. Environment variables need no flag at all: read them as $ENV.NAME or env.NAME.
How do I convert JSON to CSV with jq?
Produce one array of scalar values per row and format it with @csv, using -r so the result is raw text rather than a quoted JSON string: jq -r '.[] | [.id, .name] | @csv' users.json. For a header, emit an array of column names first and join the two expressions with a comma. @csv quotes strings and doubles any embedded quotes; @tsv separates with tabs and escapes tabs, newlines and backslashes instead. Every cell must be a string, number, boolean or null, so join or tostring any nested array or object before formatting.
Can jq edit a JSON file in place?
No. jq always writes its result to standard output and has no in-place flag like sed -i. Redirecting the output back onto the input file is worse than useless: the shell truncates the file to zero bytes before jq even starts reading it. Instead, write to a temporary file and rename it over the original with mv, which is atomic on the same filesystem, so nothing ever sees a half-written file. The sponge command from moreutils does the same job inside a pipeline, because it reads all of its input before opening the output file.
What is the difference between map(f) and .[] | f in jq?
map(f) is defined as [.[] | f]: it runs f on every element and collects the results back into a single array. Writing .[] | f instead emits each result as a separate output, which is what you want when feeding lines to a shell loop with -r. The difference shows up most with select: map(select(.ok)) returns one filtered array, while .[] | select(.ok) streams the matching items one by one. On an object, map(f) still returns an array of the values; use map_values(f) to keep the keys.