The jq command
By Flavio Copes
Learn the jq command to filter and transform JSON from curl on the CLI. Covers fields, select, map, and raw output with a real API example.
jq reads, filters, and transforms JSON on the command line. It is the tool I reach for when curl returns a large response and I need three fields from it.
You can also validate files, build new JSON objects, update arrays, and make shell scripts fail when data is missing.
Install jq
On macOS with Homebrew:
brew install jq
On Ubuntu or Debian:
sudo apt install jq
Check the installation:
jq --version
Create a small JSON file
We will use this releases.json file for the first examples:
{
"project": "notes-api",
"owner": {
"name": "Flavio",
"team": "web"
},
"releases": [
{
"version": "1.0.0",
"status": "published",
"downloads": 240
},
{
"version": "1.1.0",
"status": "draft",
"downloads": 0
},
{
"version": "2.0.0",
"status": "published",
"downloads": 510
}
]
}
Pretty-print and validate it:
jq '.' releases.json
The . filter means “return the input.” jq parses the file, then prints formatted JSON.
Invalid JSON produces an error and a non-zero exit code. This makes jq '.' a useful validation command.
Think in filters
A jq program is a filter. It receives an input value and produces zero, one, or several output values.
Filters compose with |. The output from the left becomes the input on the right:
jq '.owner | .name' releases.json
This is equivalent to:
jq '.owner.name' releases.json
The result is still valid JSON:
"Flavio"
The shell pipe and jq pipe look similar, but they operate at different levels:
curl -s https://api.github.com/repos/biomejs/biome \
| jq '.stargazers_count'
The shell sends bytes from curl to jq. Inside jq, filters pass parsed JSON values to other filters.
If command-line pipes are new to you, the free Shell Commands course explains standard input and output first. The free curl course covers the HTTP side.
Read object fields
Select a top-level field:
jq '.project' releases.json
Read a nested field:
jq '.owner.team' releases.json
Keys with spaces or punctuation need bracket syntax:
jq '.owner["display-name"]' releases.json
A missing field normally returns null:
jq '.owner.email' releases.json
Use the optional operator when a value may have the wrong shape:
jq '.owner.email?' releases.json
The ? suppresses the type error. It does not turn a missing value into an empty string.
Read arrays
Array indexes start at zero:
jq '.releases[0]' releases.json
Get the last element with a negative index:
jq '.releases[-1]' releases.json
Return a slice:
jq '.releases[0:2]' releases.json
The [] iterator emits every element as a separate result:
jq '.releases[]' releases.json
Read one field from every release:
jq '.releases[] | .version' releases.json
This produces three JSON strings, one per line.
Collect those results into one array with square brackets:
jq '[.releases[] | .version]' releases.json
Now jq produces one JSON array.
Use map to transform an array
map(filter) applies a filter to every array element and collects the results:
jq '.releases | map(.version)' releases.json
This is equivalent to the previous array construction.
Build a new shape for each item:
jq '.releases | map({version, downloads})' releases.json
The shorthand {version, downloads} reads those fields from the current object.
Rename fields explicitly:
jq '.releases | map({tag: .version, count: .downloads})' releases.json
Object construction is one of jq’s most useful features. We can reduce a large API response to the exact contract another command needs.
Filter with select
select(condition) keeps values where the condition is true.
Return published releases:
jq '.releases[] | select(.status == "published")' releases.json
Return only their versions:
jq '[
.releases[]
| select(.status == "published")
| .version
]' releases.json
jq ignores whitespace in the filter, so a long program can span several lines.
Combine conditions with and and or:
jq '[
.releases[]
| select(
.status == "published" and .downloads >= 500
)
]' releases.json
Use parentheses when a condition mixes operators. The next person should not have to remember precedence rules.
Sort and aggregate data
Sort releases by download count:
jq '.releases | sort_by(.downloads)' releases.json
Reverse the order:
jq '.releases | sort_by(.downloads) | reverse' releases.json
Add the downloads:
jq '.releases | map(.downloads) | add' releases.json
Count array items with length:
jq '.releases | length' releases.json
Find the release with the largest count:
jq '.releases | max_by(.downloads)' releases.json
These filters are clearer than extracting text and sending it through several line-oriented commands.
Provide defaults for missing values
The alternative operator // supplies a fallback for false or null:
jq '.owner.email // "not configured"' releases.json
Be careful with Boolean data. false // true returns true, because jq treats false as needing the alternative too.
Use has() when you need to distinguish a missing key from a key explicitly set to null:
jq '.owner | has("email")' releases.json
Produce raw text
By default, jq prints strings as JSON strings with quotes:
jq '.project' releases.json
Output:
"notes-api"
Use -r for raw text:
jq -r '.project' releases.json
Output:
notes-api
This is useful when another shell command expects text:
jq -r '.releases[].version' releases.json
Use compact output for one JSON value per line:
jq -c '.releases[]' releases.json
Compact JSON is still JSON. Raw output is not necessarily JSON.
Pass shell values safely
Do not inject a shell variable directly into a jq program. Pass it with --arg:
release_status=published
jq --arg status "$release_status" '
.releases[] | select(.status == $status)
' releases.json
--arg always creates a string.
Pass a JSON number, Boolean, array, or object with --argjson:
minimum=200
jq --argjson minimum "$minimum" '
.releases[] | select(.downloads >= $minimum)
' releases.json
This keeps shell quoting separate from jq syntax and avoids broken filters when a value contains spaces or punctuation.
Update JSON
Change one field with assignment:
jq '.owner.team = "platform"' releases.json
This changes jq’s output. It does not edit the input file.
Update values using their current value with |=:
jq '.releases[].downloads |= . + 10' releases.json
To replace a file, write a new file first and move it only after jq succeeds:
jq '.owner.team = "platform"' releases.json > releases.new.json && \
mv releases.new.json releases.json
Do not redirect output to the same input path. The shell truncates the file before jq can read it.
Read a stream of JSON values
jq normally reads every whitespace-separated JSON value from standard input and runs the filter once for each value.
For a JSON Lines file such as:
{"level":"info","message":"server started"}
{"level":"error","message":"database unavailable"}
select errors directly:
jq 'select(.level == "error")' app.jsonl
Use --slurp or -s to collect the input values into one array:
jq -s 'map(select(.level == "error"))' app.jsonl
Slurping loads all values into memory. For a large stream, process one value at a time instead.
jq also has a --stream mode for very large nested JSON documents. It emits paths and leaf values rather than the original object shape. That is powerful, but the filters are less readable. Use it when normal parsing genuinely exceeds memory.
Make shell scripts fail on missing data
By default, a filter that returns false or null can still leave jq with a successful status.
Use -e to connect the last output value to the exit status:
jq -e '.releases | length > 0' releases.json
The command exits with 0 when the final result is neither false nor null. It exits with 1 for false or null, and with another non-zero status for parse or program errors.
This works well in scripts:
if jq -e '.status == "ready"' deployment.json > /dev/null; then
echo 'Deployment is ready'
else
echo 'Deployment is not ready'
fi
-e checks the filter result, not just whether the input was valid JSON.
Quote jq programs correctly
On Unix shells, wrap jq programs in single quotes:
jq '.owner.name' releases.json
Many jq characters also have meaning to the shell. Single quotes keep the shell from expanding $variables, wildcards, and other syntax before jq sees it.
Put large filters in a file:
.releases
| map(select(.status == "published"))
| sort_by(.downloads)
| reverse
Save it as published.jq, then run:
jq -f published.jq releases.json
This is easier to review and version than a dense one-line filter.
Common jq mistakes
Watch for these:
- forgetting that
.items[]emits several results - using
-rwhen the next command expects JSON - interpolating shell variables instead of using
--arg - redirecting output to the input file
- treating a missing key and
nullas the same requirement - slurping a file too large for memory
- parsing JSON with
greporsedbefore trying jq - forgetting
-ewhen a filter result should control a script
How I use jq
I use jq to inspect API responses, trim deployment metadata, and validate JSON in shell scripts. I usually start with ., add one field or iterator, then shape the final object.
I keep complicated filters in a .jq file. Once a filter needs several variables and branches, I also ask whether a small JavaScript program would be clearer.
jq is at its best between tools: parse real JSON, select the exact data, and send clean JSON or text to the next command. Read JSON if you want to review the data format itself.
Want me to talk about your product? You can sponsor this site.
Related posts about cli: