Build the mental model
Bash scripts often need to talk to a web API — checking a service's health, pulling deployment status, triggering a webhook — and curl is the standard tool for making that HTTP request from the command line. curl -s makes the request quietly, without curl's usual progress meter cluttering the output, but note that -s also silences curl's own error messages, so many scripts pair it with -S (curl -sS) to keep errors visible while hiding the progress bar. You can capture the HTTP status code alongside the response body using -w "%{http_code}", or take a shortcut with -f, which makes curl itself return a nonzero exit status on 4xx/5xx responses — handy because a set -e script then stops automatically on a bad response, though -f also suppresses the body on error, so if you need to see what the server actually said, capturing status and body separately works better. Once you have a JSON body, curl can't do anything more with it — that's where jq comes in. jq '.field' prints a field as JSON, so a string value comes back wrapped in quotes exactly as JSON encodes it; jq -r '.field' prints the same value 'raw', with the quotes stripped, which is what you almost always want when assigning the result into a bash variable, since otherwise those literal quote characters end up baked into the value. jq also indexes into arrays with .roles[0], chains lookups together, and pretty-prints an entire object when given just '.' — and like curl, jq itself exits nonzero on invalid JSON, which a set -e script will also catch.
Connect it to a real scenario
Say you're writing a deploy script that pings a status API before rolling out, extracts the currently deployed version, and only proceeds if it differs from the new version being deployed — skipping unnecessary redeploys. The script uses curl -sf to fetch the JSON status, which fails loudly with a nonzero exit if the API is unreachable or returns an error, letting set -e halt the deploy right there instead of proceeding against a service that might not even be up. Once the JSON body is in hand, jq -r '.version' pulls out just the version string, stripped of its surrounding quotes, so it can be compared directly against the new version with a plain bash string comparison like [ "$current_version" = "$new_version" ]. Without jq, you'd be stuck trying to extract that one field with sed or grep and a regex, which technically works until the API's formatting changes even slightly — a field reordered, extra whitespace added, a nested object introduced — at which point a regex silently breaks or matches the wrong thing, while jq keeps working because it actually parses the JSON structure rather than pattern-matching against its text.
Try the working example
#!/bin/bash
set -euo pipefail
# In a real script you would fetch this with curl, e.g.:
# response=$(curl -sf -w "\n%{http_code}" "https://api.example.com/users/42")
# http_code=$(echo "$response" | tail -n1)
# body=$(echo "$response" | sed '$d')
# [ "$http_code" = "200" ] || { echo "request failed: $http_code" >&2; exit 1; }
#
# Here we use a literal JSON string standing in for that response body,
# so the example is deterministic and needs no network access.
response='{"id":42,"name":"Ada Lovelace","active":true,"roles":["admin","editor"]}'
echo "Full response:"
echo "$response" | jq '.'
name=$(echo "$response" | jq -r '.name')
echo "Extracted name: $name"
first_role=$(echo "$response" | jq -r '.roles[0]')
echo "First role: $first_role"
is_active=$(echo "$response" | jq '.active')
echo "Active field (raw JSON value): $is_active"
Running the script above prints:
Full response:
{
"id": 42,
"name": "Ada Lovelace",
"active": true,
"roles": [
"admin",
"editor"
]
}
Extracted name: Ada Lovelace
First role: admin
Active field (raw JSON value): true5-minute try-it
Extend the jq example to pull out the second role from the roles array instead of the first, and add a line that prints how many roles the array contains using jq's length filter.
One important caution
Forgetting -r and assigning jq's quoted JSON string output straight into a bash variable, ending up with literal double quotes baked into the value
Not checking curl's exit status or HTTP code before piping its output into jq — an error page or empty body from a failed request makes jq fail with a confusing 'parse error' far from the real cause
jq Manual — Bash / Shell Scripting