jq is JSON's command line
jq is a small, fast command-line tool for slicing, filtering, and reshaping JSON — think of it as sed/awk for JSON. It reads JSON on stdin, applies a filter you write, and prints JSON (or raw text) on stdout, which makes it the go-to for exploring API responses, log files, and config from a terminal. This is the recipe-focused companion to the concept-level JSONPath guide; to try any filter below interactively, paste your JSON into the jq playground.
Every example assumes this input in data.json:
{
"users": [
{ "name": "Ada", "age": 36, "role": "admin", "tags": ["a", "b"] },
{ "name": "Ravi", "age": 29, "role": "member", "tags": ["b"] },
{ "name": "Mei", "age": 41, "role": "admin", "tags": [] }
]
}The basics: access and pipe
The filter . is the identity — it returns the input unchanged, which is why jq . data.json is the fastest way to pretty-print any JSON. From there you drill in with dots and pipe (|) filters together:
jq '.' # pretty-print the whole document
jq '.users' # the users array
jq '.users[0]' # first user object
jq '.users[0].name' # "Ada"
jq '.users | length' # 3 — pipe the array into lengthIterate an array with .[]
.[] explodes an array into a stream of its elements — the heart of most jq one-liners. Pipe that stream into another filter to act on each element:
jq '.users[].name' # "Ada" "Ravi" "Mei" (one per line)
jq '.users[] | .role' # "admin" "member" "admin"
jq '[.users[].name]' # ["Ada","Ravi","Mei"] — wrap the stream back into an arrayThe bracket-wrapping trick — [ ... ] around a stream — is how you collect results back into a single array.
Filter with select()
select(condition) passes through only the elements where the condition is true. Combine it with .[] to filter an array:
# users older than 30
jq '.users[] | select(.age > 30)'
# just the names of admins
jq '.users[] | select(.role == "admin") | .name' # "Ada" "Mei"
# users who have any tags
jq '.users[] | select(.tags | length > 0)'Transform with map() and object construction
map(f) applies a filter to every element of an array and returns a new array. Build new objects with { key: filter } to reshape data:
# pull one field from each element
jq '.users | map(.name)' # ["Ada","Ravi","Mei"]
# reshape each user into a smaller object
jq '.users | map({ person: .name, admin: (.role == "admin") })'
# add a computed field
jq '.users | map(. + { senior: (.age >= 40) })'Aggregate: group_by, sort_by, unique, add
The recipes that replace a script:
jq '.users | group_by(.role)' # array of arrays, grouped by role
jq '[.users[].age] | add' # 106 — sum of ages
jq '[.users[].age] | add / length' # average age
jq '.users | sort_by(.age)' # ascending by age
jq '.users | sort_by(-.age)' # descending
jq '[.users[].role] | unique' # ["admin","member"]
jq '.users | max_by(.age) | .name' # "Mei"Keys, values, and to_entries
Inspect and iterate object shapes:
jq '.users[0] | keys' # ["age","name","role","tags"]
jq '.users[0] | to_entries' # [{"key":"name","value":"Ada"}, ...]
jq '.. | .name? // empty' # every "name" anywhere (recursive descent).. is recursive descent — it walks every value at every depth, handy when you don't know how deeply a field is nested. The // empty swallows the misses, and // in general is the alternative operator ("use the left value, or the right if the left is null/false").
Raw and CSV output for pipelines
By default jq prints JSON (strings get quotes). -r prints raw strings — essential when feeding other shell tools — and @csv/@tsv format arrays as delimited rows:
jq -r '.users[].name' # Ada Ravi Mei (no quotes)
# turn objects into CSV rows
jq -r '.users[] | [.name, .age, .role] | @csv' # "Ada",36,"admin" ...That last recipe is a one-line JSON-to-CSV converter for flat records.
Working with NDJSON and multiple files
jq reads a stream of JSON values, so it handles NDJSON (one object per line) natively — no array needed:
# process each line of a log file, keep errors
jq -c 'select(.level == "error")' app.ndjson # -c = compact, one line out per match
# slurp separate values into one array with -s
jq -s 'add' part1.json part2.json-c (compact) keeps output as one line per value — the right form for producing NDJSON — and -s (slurp) reads the entire input into a single array first.
The handful worth memorizing
| Goal | Filter |
|---|---|
| Pretty-print | jq . |
| A field | .a.b |
| Every array element | .arr[] |
| Filter | .arr[] \| select(.x > 1) |
| Reshape each | .arr \| map({k: .v}) |
| Sum | [.arr[].n] \| add |
| Raw text out | jq -r |
| CSV row | [.a, .b] \| @csv |
Master those eight and you can answer most "get X out of this JSON" questions in one line. For the query-language view of the same problems see the JSONPath guide and the JSONPath tester; to experiment with jq itself, use the jq playground.