jq Explained: Querying JSON From the Command Line
Every modern command-line tool emits JSON. Container runtimes, cloud CLIs, package managers, and half the internet’s APIs all return structured data, and reaching for grep on it works right up until the formatting changes.
jq is a small language purpose-built for the job. You will use about ten percent of it constantly.
sudo apt install jq # Debian and Ubuntu
sudo dnf install jq # Fedora
sudo pacman -S jq # Arch
Pretty-printing, the zero-effort win
curl -s https://api.example.com/data | jq
With no filter, jq validates, indents, and colourises. That alone justifies installing it, because reading a minified API response otherwise is miserable.
The identity filter . does the same thing explicitly:
jq . file.json
Selecting fields
echo '{"name":"nginx","state":"running","pid":1234}' | jq '.name'
# "nginx"
echo '{"name":"nginx","state":"running"}' | jq -r '.name'
# nginx
-r gives raw output without quotes, which is what you want whenever the result goes into a variable or another command.
Nested paths chain with dots:
jq '.server.config.port' config.json
Arrays
jq '.[0]' # first element
jq '.[-1]' # last element
jq '.[2:5]' # a slice
jq '.[]' # each element, as separate outputs
jq 'length' # how many
The distinction between .[] and [.[]] matters. The first emits each element as its own JSON document; the second collects them back into an array. Output arriving as several documents when you expected one array is almost always a missing pair of brackets.
Mapping and filtering
# Pull one field from every element
jq -r '.[].name' containers.json
# Or explicitly
jq -r 'map(.name) | .[]' containers.json
# Only the ones matching a condition
jq '.[] | select(.state == "running")'
# Combine
jq -r '.[] | select(.state == "running") | .name'
select is the workhorse. It passes through inputs where the expression is true and drops the rest.
# Numeric comparison
jq '.[] | select(.memory_mb > 512)'
# Substring match
jq '.[] | select(.name | test("nginx"))'
# Multiple conditions
jq '.[] | select(.state == "running" and .restarts > 0)'
Building new shapes
jq '{name: .name, port: .config.port}'
jq '.[] | {name, state}' # shorthand when keys match
jq -r '.[] | [.name, .state] | @tsv' # tab separated, for column
The @tsv and @csv formatters are how you get from JSON into something column -t, awk, or a spreadsheet can use:
docker inspect $(docker ps -q) \
| jq -r '.[] | [.Name, .State.Status, .Config.Image] | @tsv' \
| column -t
Real examples
# Which containers are running, and from what image
docker ps --format '{{json .}}' | jq -r '[.Names, .Image] | @tsv'
# systemd units that failed
systemctl list-units --failed --output=json | jq -r '.[].unit'
# Package versions from an API
curl -s https://api.github.com/repos/torvalds/linux/tags \
| jq -r '.[0:5] | .[].name'
# Extract a value into a shell variable
VERSION=$(curl -s "$API/version" | jq -r '.version')
# Sum a field across an array
jq '[.[].size_bytes] | add'
# Group and count
jq -r 'group_by(.state) | .[] | "\(.[0].state) \(length)"'
Defaults and missing keys
A missing key evaluates to null, which then breaks whatever you do with it.
jq -r '.name // "unknown"' # fall back if null or false
jq -r '.a.b?' # suppress the error, emit nothing
jq 'has("name")' # test for the key
The // operator triggers on null and false, which is worth knowing if false is a legitimate value you want to keep.
Variables and multiple inputs
# Pass a shell value in safely
jq --arg svc "nginx" '.[] | select(.name == $svc)'
# Numbers need --argjson
jq --argjson limit 100 '.[] | select(.size > $limit)'
Use --arg rather than interpolating into the filter string. Building a filter with shell interpolation has the same class of problem as building SQL that way.
For newline-delimited JSON, common in log output, use -s to slurp into an array or process each line as it comes:
cat events.jsonl | jq -r 'select(.level == "error") | .message'
cat events.jsonl | jq -s 'length'
When jq is the wrong tool
For genuinely complex transformation, a few lines of Python are easier to read and to maintain than a dense jq expression. jq is excellent for extraction and filtering in a pipeline; it becomes write-only fairly quickly when the logic grows.
The rule of thumb: if the filter no longer fits comfortably on one line, consider whether it belongs in a script instead.
Frequently Asked Questions
What does jq do?
It parses JSON, applies a filter expression you write, and prints the result. Think of it as a query language and transformation tool for JSON, in the way awk is for columnar text. It also pretty-prints and colourises by default.
Why not just use grep on JSON?
Because JSON has structure that text tools cannot see. Whitespace and key order can change without changing meaning, values can nest arbitrarily, and a string can contain characters that look like syntax. grep works until the formatting changes, then breaks silently.
What is the difference between .[] and [.[]]?
The .[] filter iterates an array or object and emits each element as a separate output. Wrapping it in brackets as [.[]] collects the results back into an array. Forgetting the wrapper is why output sometimes arrives as several JSON documents rather than one array.
How do I get raw strings instead of quoted ones?
Use -r, which prints strings without JSON quoting. This is what you want when feeding output into another command or a shell variable, because otherwise the quotes are part of the value.
How do I handle keys that might not exist?
Use the alternative operator to supply a default, as in .name // "unknown", or add a question mark to suppress errors on missing paths with .a.b?. Without either, a missing key produces null and a type error further along the pipeline.
Is jq installed by default?
Rarely. It is packaged everywhere though, and it is a single static binary, so installing it on a server you administer is quick and worth doing before you need it at three in the morning.