Every JSON API response is one pipe away from being actually readable. jq turns curl’s wall of braces into the two fields you wanted.
jq is a functional language for JSON that fits in a single binary — you feed it JSON on stdin, hand it a filter expression, and it streams back exactly the shape of data you asked for. That sounds modest until you realize how much of shell scripting is "fetch JSON, pull out a couple of values, maybe reshape it": API responses from curl, cloud CLI output, log files, package manifests, webhook payloads. Without jq you’re doing that with grep and a prayer; with it, one expression replaces twenty lines of sed. The mental shift is that jq is not a text tool — it treats input as structured values, so quoting, nesting, and type errors become explicit instead of silent. Master a handful of built-ins (.[], select(), map(), //) and you stop fearing any API that talks JSON.
install
# macOS
brew install jq
# Debian/Ubuntu
sudo apt install jq
# Fedora
sudo dnf install jq
# Arch
sudo pacman -S jq
examples
# Pretty-print an API response (the default is 2-space indented)
curl -s https://api.github.com/repos/stedolan/jq | jq
# Pull one field, raw — -r drops the quotes so you can actually use the value
curl -s https://api.github.com/repos/stedolan/jq | jq -r '.description'
# Iterate an array and filter it: .[] streams elements, select() keeps matches
jq '.[] | select(.state == "open") | .title' issues.json
# Rebuild objects: wrap fields you want in {} to emit a slimmed-down payload
jq '[.[] | {name, stars: .stargazers_count}]' repos.json
# // is the alternative operator — fallback when a field is null or missing
jq -r '.user.login // "unknown-user"' event.json
# -e sets the exit code from the output — null/false exit 1, so scripts can branch
jq -e '.result.ok' response.json || echo "request failed"
# --arg passes shell vars in safely (no string-interpolation injection nonsense)
jq --arg q "$query" '.[] | select(.name | contains($q))' data.json
pro tip
-S sorts object keys on output, which turns diffs between two similar JSON files from noise into signal — diff <(jq -S a.json) <(jq -S b.json) is a JSON diff with zero extra tooling. Also keep -r burned in: forgetting it is the #1 reason people think jq "adds quotes" to everything.
honest limits
jq is JSON-only. It will not parse YAML, XML, or your config file with comments (write yaml/jq forks exist, but they’re separate tools with their own quirks). Streaming huge files works but memory behavior on multi-GB inputs is not its calling — that’s what jq --stream or mlr are for. And don’t use jq to *generate* config files by hand-editing strings; that’s a job for a template. If your data is CSV, xsv/miller will make you happier. If you just need one nested value from a known path, grep -o is faster to type and you’ll survive.