Quick answer: jq is a command-line JSON processor. The five filters that cover most daily work are
.keyto read a field,.[]to iterate an array,select(condition)to filter,map(filter)to transform every element, and the-rflag to print strings without quotes. Chain them with|:jq -r '.[] | select(.active) | .email' users.jsonprints the email of every active user, one per line.
You have a 3,000-line API response in the terminal and need the IDs of every record where status is failed. Scrolling is not an option and writing a Python script feels heavy. jq does it in one line, and the forty commands below are the ones that come up again and again. Each row shows the filter, what it does, a tiny input, and the output you will see.
Run a sample expression in the JSONPath Query Tester, or explore the same nested data in the JSON Viewer.
Install jq and Try a Filter
jq is a single binary with no dependencies. Package managers carry it on every platform:
brew install jq # macOS
sudo apt install jq # Debian, Ubuntu
winget install jqlang.jq # Windows
jq --version
The fastest way to learn a filter is to run it against a literal with -n, which starts from null input, so you do not need a file at all:
jq -n '[1, 2, 3] | map(. * 10) | add'
# 60
echo '{"users": [{"name": "Ada"}, {"name": "Bob"}]}' | jq -r '.users[].name'
# Ada
# Bob
How to Read This Cheat Sheet
jq works like a Unix pipeline inside a single program: each filter takes a JSON value in and emits zero or more values out, and | connects them. The command shape is always jq [flags] 'filter' [file], with input from stdin when no file is given. Flags such as -r go before the filter. Rows that start with jq show the full command because the flag is the point; the rest show only the filter.
Basics: Selecting Values
Wrap the filter in single quotes on the shell. Every example assumes the input arrives on stdin or as a file argument.
| # | Filter | What it does | Input | Output |
|---|---|---|---|---|
| 1 | . | Identity: outputs the input, pretty-printed | {"a":1} | { "a": 1 } |
| 2 | .name | Value of a key; null if missing | {"name":"Ada"} | "Ada" |
| 3 | .user.email | Nested path | {"user":{"email":"ada@x.io"}} | "ada@x.io" |
| 4 | .items[0] | Array index; .items[-1] is the last element | {"items":[5,6]} | 5 |
| 5 | .items[] | Iterate: one output per element | {"items":[5,6]} | 5 6 |
| 6 | keys | Sorted key names (keys_unsorted keeps file order) | {"b":1,"a":2} | ["a","b"] |
| 7 | length | Array length, string length, or key count | [1,2,3] | 3 |
Building and Filtering
select keeps values that pass a test; map applies a filter to every element and returns a new array.
| # | Filter | What it does | Input | Output |
|---|---|---|---|---|
| 8 | [.a, .b] | Collect several outputs into an array | {"a":1,"b":2} | [1,2] |
| 9 | {name, id: .user_id} | Build an object; a bare key copies that field | {"name":"Ada","user_id":7} | {"name":"Ada","id":7} |
| 10 | .[] | select(.age > 30) | Keep only values that pass the test | [{"age":25},{"age":40}] | {"age":40} |
| 11 | map(.price * 2) | Apply a filter to every element | [{"price":2},{"price":5}] | [4,10] |
| 12 | map(select(.active)) | Filter but keep the array wrapper | [{"active":true},{"active":false}] | [{"active":true}] |
| 13 | del(.password) | Remove a key (del(.[0]) removes by index) | {"id":1,"password":"x"} | {"id":1} |
Output Flags and Formats
These are command-line flags and @format strings. -r is the one you will reach for most: without it every string output keeps its quotes.
| # | Filter | What it does | Input | Output |
|---|---|---|---|---|
| 14 | jq -r '.name' | Raw output: strings without surrounding quotes | {"name":"Ada"} | Ada |
| 15 | jq -c '.' | Compact: one line per value (JSON Lines output) | { "a": [1, 2] } | {"a":[1,2]} |
| 16 | jq -S '.' | Sort keys recursively on output | {"b":1,"a":2} | { "a": 2, "b": 1 } |
| 17 | jq -e '.ok' | Exit code 1 if the last output is false or null | {"ok":false} | false (exit status 1) |
| 18 | jq -r '.[] | @csv' | Each inner array becomes a CSV line | [["Ada",1]] | "Ada",1 |
| 19 | jq -r '.[] | @tsv' | Tab-separated, no quoting, tabs escaped | [["Ada",1]] | Ada<TAB>1 |
Transforming Objects and Arrays
to_entries and with_entries are the escape hatch whenever you need to treat keys as data.
| # | Filter | What it does | Input | Output |
|---|---|---|---|---|
| 20 | to_entries | Object to array of key/value pairs | {"a":1} | [{"key":"a","value":1}] |
| 21 | with_entries(.key |= ascii_upcase) | Edit keys or values, then back to an object | {"a":1} | {"A":1} |
| 22 | group_by(.t) | Array of arrays, grouped and sorted by the field | [{"t":"x"},{"t":"y"},{"t":"x"}] | [[{"t":"x"},{"t":"x"}],[{"t":"y"}]] |
| 23 | sort_by(.age) | Sort by a field; add | reverse for descending | [{"age":40},{"age":25}] | [{"age":25},{"age":40}] |
| 24 | unique_by(.id) | Deduplicate by a field (unique for scalars) | [{"id":1},{"id":1},{"id":2}] | [{"id":1},{"id":2}] |
| 25 | add | Sum numbers, concatenate strings, or merge objects | [1,2,3] | 6 |
| 26 | .count |= . + 1 | Update in place (= assigns, += adds) | {"count":1} | {"count":2} |
Strings and Math
jq string functions use regular expressions in the Oniguruma dialect, so most PCRE patterns work.
| # | Filter | What it does | Input | Output |
|---|---|---|---|---|
| 27 | tonumber | String to number (tostring goes the other way) | "42" | 42 |
| 28 | split(",") | String to array | "a,b" | ["a","b"] |
| 29 | join("-") | Array to string | ["a","b"] | "a-b" |
| 30 | gsub("a"; "o") | Regex replace all (sub replaces the first) | "banana" | "bonono" |
| 31 | "\(.first) \(.last)" | String interpolation | {"first":"Ada","last":"L"} | "Ada L" |
Conditionals and Defaults
The // operator is the fastest way to supply defaults for missing or null fields.
| # | Filter | What it does | Input | Output |
|---|---|---|---|---|
| 32 | if .n > 1 then "many" else "one" end | Branching; elif is allowed, end is required | {"n":3} | "many" |
| 33 | .nick // .name | Alternative when the left side is null or false | {"name":"Ada"} | "Ada" |
| 34 | try tonumber catch "NaN" | Catch an error; try alone yields nothing on error | "abc" | "NaN" |
Variables and Shell Arguments
--arg always binds a string. Use --argjson when you need a number, boolean, or object.
| # | Filter | What it does | Input | Output |
|---|---|---|---|---|
| 35 | jq --arg n Ada '.[] | select(.name == $n)' | Pass a string from the shell as $n | [{"name":"Ada"},{"name":"Bob"}] | {"name":"Ada"} |
| 36 | jq --argjson min 30 'map(select(.age >= $min))' | Pass a JSON value from the shell | [{"age":25},{"age":40}] | [{"age":40}] |
| 37 | jq --slurpfile c cfg.json '.env = $c[0].env' | Bind a file's values as an array in $c | {"a":1} + cfg.json: {"env":"prod"} | {"a":1,"env":"prod"} |
Input and Output Streams
jq treats a file of concatenated values (including JSON Lines) as a stream and runs the filter once per value.
| # | Filter | What it does | Input | Output |
|---|---|---|---|---|
| 38 | jq -s '.' | Slurp every input value into one array | {"a":1} {"a":2} | [{"a":1},{"a":2}] |
| 39 | jq -R '.' | Raw input: each text line becomes a string | hello | "hello" |
| 40 | jq -c '.[]' | Explode an array into one value per line | [1,2] | 1 2 |
A Few Worth Knowing Beyond the Forty
..walks every value in the tree recursively, so[.. | numbers]collects every number anywhere in the document.has("key")tests for a key even when its value is null, which.key != nullcannot distinguish.jq -n '[inputs]'does what-sdoes but streams, which is the form the jq manual recommends for large files.reduce .[] as $x (0; . + $x)is the general fold whenaddis not enough.@base64,@uri,@sh, and@jsonsit alongside@csvand@tsvas output formatters.--taband--indent 2control pretty-printing;-jjoins raw output without newlines.
Reading JSON From curl
The most common place jq sits is immediately after curl. Use curl's -s so the progress meter does not pollute the stream:
# Pick two fields from one object
curl -s https://api.github.com/repos/jqlang/jq | jq '{name, stars: .stargazers_count}'
# Authenticated request, tab-separated output for a shell loop
curl -s -H "Authorization: Bearer $TOKEN" https://api.example.com/users \
| jq -r '.[] | [.id, .email] | @tsv'
# Fail loudly if the response is not JSON (an HTML error page, say)
curl -s https://api.example.com/health | jq -e '.status == "ok"' > /dev/null || echo "unhealthy"
When a request needs a JSON body rather than just reading one, the sending JSON with curl guide covers headers, files, and the Windows quoting problems. For a long response where you want to look rather than script, pipe through jq . or paste it into JSON Prettifier, where the collapsible tree view and syntax highlighting make a 3,000-line payload readable.
Patterns You Will Reuse
Combinations that come up in real work, built from the rows above:
# Count records per status
jq 'group_by(.status) | map({status: .[0].status, count: length})' orders.json
# CSV with a header row
jq -r '["id","email"], (.[] | [.id, .email]) | @csv' users.json
# Records missing a required key
jq -c '.[] | select(has("email") | not)' users.json
# Rename a key everywhere
jq 'map(with_entries(if .key == "userId" then .key = "user_id" else . end))' events.json
# Convert an object of objects into an array with the key inside
jq 'to_entries | map(.value + {id: .key})' lookup.json
# Deep-merge a defaults file into each record
jq --slurpfile d defaults.json 'map($d[0] * .)' records.json
The last one uses * on objects, which merges recursively, with the right-hand side winning on conflicts. For converting between JSON arrays and one-record-per-line files, the JSON Lines guide has a dedicated table of jq commands, and the JSON to CSV guide goes deeper on @csv with nested data.
Gotchas
- Forgetting
-r.NAME=$(jq '.name' f.json)stores"Ada"with the quotes. Add-rwhenever the result goes to the shell. Cannot index array with "name". The input is an array and the filter expects an object. Put.[]ormap(...)in front.Cannot iterate over null..items[]ran on a record with noitemskey. Write.items[]?to skip silently or(.items // [])[]to default to an empty array.--argis always a string.select(.age > $min)with--arg min 30compares a number to a string and matches nothing. Use--argjson.- Windows quoting. cmd.exe does not treat single quotes as quotes. Use double quotes and escape inner ones:
jq ".[] | select(.name == \"Ada\")", or put the filter in a file and use-f filter.jq. In PowerShell, single quotes work but calljq.exeexplicitly. - Invalid input. jq stops at the first parse error with a line number. If the file came from a hand-edited config, a trailing comma is the usual culprit; the common JSON errors guide lists the rest.
- Keys with dashes or spaces.
.user-idis parsed as subtraction. Write.["user-id"].