| Tested on | RHEL 10.2 (Coughlan) |
|---|---|
| Package | bash 5.2.26-6.el10jq 1.7.1-11.el10_2.2 |
| Applies to | Linux distributions with Bash installed |
| Privilege | Normal user |
| Scope | Parse and filter JSON in Bash with jq: scalars, nested keys, arrays, select, missing keys and null, --arg / --argjson, jq -n for safe JSON construction, jq -e exit status, jq empty for syntax checks, curl | jq, and Bash loops over jq -c records. Does not cover full jq language reference or streaming large files with jq --stream. |
| Related guides | Bash best practices Shell scripting course hub |
API responses, config files, and kubectl/docker JSON output are not line-oriented text. Let jq handle JSON structure and pass only the values the shell needs.
Read a key from a file:
name=$(jq -r '.name' data.json)
echo "$name"For JSON already in a Bash variable:
json='{"name":"alice","id":42}'
name=$(jq -r '.name' <<<"$json")
echo "$name"alicejq parses the JSON structure, .name selects the key, -r removes JSON quotes from string output, and $(...) stores the result in a Bash variable.
Install jq
| Distribution | Command |
|---|---|
| RHEL / Rocky / Alma | sudo dnf install jq |
| Fedora | sudo dnf install jq |
| Ubuntu / Debian | sudo apt install jq |
| Alpine | apk add jq |
| macOS | brew install jq |
Confirm the command is available:
jq --versionjq-1.7.1Common jq Patterns for Bash
| Need | jq filter |
|---|---|
| Entire document | . |
| Read key | .name |
| Nested key | .owner.email |
| First array item | .services[0] |
| Every array item | .services[] |
| Plain string | -r '.name' |
| Default for null | .port // "n/a" |
| Filter array | .services[] | select(.active) |
| Compact object | -c |
| Test result in Bash | -e |
Read JSON from a File, Variable, or Command
JSON file:
jq -r '.name' app.jsonBash variable or string:
json='{"name":"alice","id":42}'
jq -r '.name' <<<"$json"Command or API output:
curl -fsS https://httpbin.org/json | jq -r '.slideshow.author'Without pipefail, the pipeline's status normally comes from jq, so a failed curl can be hidden if jq itself exits successfully. For scripts that must detect HTTP failures reliably, capture and check curl separately as shown later, or enable pipefail as described in Bash pipes before using curl | jq.
Stream directly when you only need jq output. Capture the response in a variable when several later operations need the same document.
Create the Sample JSON
The examples below use this file saved as app.json:
cat > app.json << 'EOF'
{
"name": "deploy-kit",
"version": "2.4.1",
"active": true,
"owner": {
"username": "alice",
"email": "alice@example.com"
},
"services": [
{"name": "api", "port": 8080, "active": true},
{"name": "worker", "port": null, "active": false},
{"name": "cache", "port": 6379, "active": true}
],
"notes": ""
}
EOFPretty-print the document to inspect its structure:
jq '.' app.json{
"name": "deploy-kit",
"version": "2.4.1",
"active": true,. means “the current input value.” With no further filter, jq parses and pretty-prints the document.
Extract JSON Values and Nested Keys
Read a scalar with a dot path:
jq '.owner.username' app.json"alice"Without -r, jq prints JSON syntax and keeps quotes around strings. For Bash variables, paths, and command arguments, use -r (raw output):
jq -r '.owner.username' app.jsonalice-r mainly matters for string results: it prints the string contents without JSON quotes. Numbers, Booleans, and null are already printed without surrounding quotes. -r is raw output — do not confuse it with uppercase -R, which treats the input as raw text instead of parsing JSON.
-r does not convert JSON null to an empty Bash value. For example, jq -r '.missing' prints the literal text null. Handle missing or null values explicitly when assigning them to shell variables.
Nested keys chain with dots:
jq -r '.owner.email' app.jsonalice@example.comBracket notation helps when a key contains special characters: .["owner"]["email"] matches .owner.email for simple names.
Command substitution works for a single scalar: user=$(jq -r '.owner.username' app.json). When you need several fields, extract them in one jq invocation instead of running jq separately for every field:
jq -r '[.owner.username, .owner.email] | @tsv' app.jsonalice alice@example.comWork with JSON Arrays
Read one element by index:
jq -r '.services[0].name' app.jsonapiEmit every element:
jq -r '.services[].name' app.jsonapi
worker
cacheCount items:
jq '.services | length' app.json3map reshapes each element. Active service names in a JSON array:
jq '.services | map(select(.active) | .name)' app.json[
"api",
"cache"
]Inside jq, | sends the output of one filter to the next. It is jq syntax, not a Bash pipe.
Compact output (-c) puts each JSON value on one line. That matters when Bash loops over records:
jq -c '.services[]' app.json{"name":"api","port":8080,"active":true}
{"name":"worker","port":null,"active":false}
{"name":"cache","port":6379,"active":true}Each line is one valid JSON object you can pass to another jq invocation or store in a variable.
Filter JSON with select
select(condition) keeps elements that match. List only active services:
jq -c '.services[] | select(.active)' app.json{"name":"api","port":8080,"active":true}
{"name":"cache","port":6379,"active":true}For a Boolean field, select(.active) is equivalent here to select(.active == true). In jq conditions, false and null are false; other values are treated as true.
Combine select with field extraction in one filter program instead of filtering in Bash with string compares.
Handle Missing Keys, null, false, and Empty Strings
JSON null, a missing key, and an empty string "" are three different cases. A simple field lookup returns null for both a missing key and a present key with JSON null:
jq '.owner.phone' app.jsonnulljq '.services[1].port' app.jsonnullUse has() to tell whether a key exists:
jq '.owner | has("phone")' app.jsonfalsejq '.services[1] | has("port")' app.jsontrueA missing object key already produces null. The optional operator ? is useful when an operation could otherwise raise an error — for example, attempting object access on a value that is not an object. More generally, EXP? suppresses errors from EXP. It does not replace has() when you need to know whether a key exists.
The alternative operator // supplies a default when the left side is null or false:
jq -r '.owner.phone // "n/a"' app.jsonn/a// treats both null and false as absent for fallback purposes. Do not use // when false is a meaningful value you need to preserve:
jq '.services[1].active // true' app.jsontrueactive exists and is explicitly false, but // still returns true.
The notes field exists but is an empty string, not missing and not null:
jq -r 'if .notes == "" then "(empty string)" else .notes end' app.json(empty string)Check the key you care about and whether the API uses null, omission, or "" before you pick has(), //, or an explicit test.
Use jq Exit Status in Bash with -e
By default, a successfully executed jq program normally exits 0 regardless of whether its output is useful as a Bash condition. Add -e when you want jq's result to determine the exit status. With -e, jq exits 0 when the last output is neither false nor null, 1 when the last output is false or null, and 4 when the filter produces no output.
.active returns the Boolean value true, so jq -e exits 0:
jq -e '.active' app.jsontrueWhen a filter emits multiple values, -e uses the last output value to determine status. Use an explicit aggregate such as any(...) when your condition means “at least one item matches.” Check that at least one active service exists:
jq -e 'any(.services[]; .active)' app.json >/dev/null
echo "exit=$?"exit=0Use select() when you want matching records; use any() when you want a yes/no result. When the filter produces no output, jq -e exits 4:
jq -e '.services[] | select(.name == "missing")' app.json > /dev/null
echo "exit=$?"exit=4Use jq -e when a query result should control Bash flow. Do not use jq -e . to validate JSON syntax — a valid document whose top-level value is false or null also makes jq -e exit 1. See Bash exit codes for how $? propagates in larger scripts.
Pass Bash Variables Safely with --arg and --argjson
Do not build jq filters by inserting shell variables into the filter text. Pass strings with --arg and JSON values with --argjson so jq handles the quoting and type correctly.
Pass strings with --arg name value. The value is available inside the filter as $name:
u=alice
jq -r --arg name "$u" '.owner.username | select(. == $name)' app.jsonaliceUse --argjson when the shell value should be parsed as a JSON value such as a number, Boolean, null, array, or object. --argjson port 8080 produces a JSON number; --arg port "8080" produces a JSON string:
jq -n --arg svc api --argjson port 8080 '{service: $svc, port: $port}'{
"service": "api",
"port": 8080
}For a numeric port read from JSON, capture it in Bash first, then pass it as JSON:
PORT=$(jq -er '.services[] | select(.name=="api") | .port' app.json) || {
echo "API port not found" >&2
exit 1
}
jq -n --arg app deploy-kit --argjson port "$PORT" '{application: $app, listen_port: $port}'{
"application": "deploy-kit",
"listen_port": 8080
}--argjson parses its argument as JSON, while --arg always creates a JSON string. Keep shell expansions quoted: --argjson port "$PORT". The example first uses jq -er so a missing or null port stops the script instead of passing invalid input to --argjson. This pattern is appropriate for a required numeric value; jq -e should not be used this way when false is a legitimate result.
Build JSON Safely with jq -n
jq -n starts from null input and runs a filter that builds new JSON. Combine it with --arg to create request bodies without manual quoting.
TOKEN=sec123
jq -n --arg token "$TOKEN" '{auth: {type: "bearer", token: $token}}'{
"auth": {
"type": "bearer",
"token": "sec123"
}
}Avoid printing request bodies that contain passwords, tokens, or other secrets.
Loop Over JSON Arrays in Bash
jq -c emits each object on one line, so while IFS= read -r can read one complete JSON object per iteration. See read a file line by line for more read-loop patterns.
while IFS= read -r obj; do
name=$(jq -r '.name' <<<"$obj")
port=$(jq -r '.port // "n/a"' <<<"$obj")
echo "service=$name port=$port"
done < <(jq -c '.services[]' app.json)service=api port=8080
service=worker port=n/a
service=cache port=6379<<<"$obj" feeds that compact object back into jq without creating a temporary file.
Avoid for item in $(jq ...); — command substitution followed by word splitting can break JSON or string values containing whitespace. Use jq -c with while IFS= read -r instead.
For large arrays, extract the fields you need in one jq invocation rather than starting jq once per field for every object:
jq -r '.services[] | [.name, (.port // "n/a")] | @tsv' app.jsonapi 8080
worker n/a
cache 6379@tsv is convenient when you intentionally want tab-separated scalar output rather than JSON. Keep jq -c when downstream code needs the original JSON types or object structure.
Validate JSON Input
Validate JSON syntax — jq empty parses the input and emits nothing. empty produces no output, but jq must still parse the complete input, so malformed JSON causes a non-zero exit without printing the document:
if jq empty app.json 2>/dev/null; then
echo "valid JSON"
else
echo "invalid JSON"
fivalid JSONMalformed input makes jq exit non-zero and print a parse error:
printf '%s\n' '{bad json' | jq .jq: parse error: Invalid numeric literal at line 1, column 5With jq 1.7.1 used here, this parse error returned status 5. Scripts should generally test for a non-zero status rather than depend on that specific parse-error code.
Test whether a jq query result is true or non-null — use jq -e:
jq -e '.active' app.json >/dev/null && echo "active is true"Test whether a record exists:
jq -e 'any(.services[]; .name == "api")' app.json >/dev/null && echo "api service found"When JSON comes from an HTTP request via curl, check the download before parsing. An error page often returns HTML that jq cannot parse:
URL='https://httpbin.org/json'
response=$(curl -fsS "$URL") || {
echo "request failed" >&2
exit 1
}
jq -r '.slideshow.author' <<<"$response"curl -f normally returns a non-zero status for HTTP responses with status 400 or higher and suppresses the response body, preventing many HTTP error pages from reaching jq. A successful HTTP transfer still does not guarantee that the body contains valid JSON.
Why grep, sed, and awk Are Not JSON Parsers
Bash does not include a native JSON parser. Use jq for structured JSON instead of grep, sed, or awk on the document itself — see grep, sed, and awk pipelines for when those line tools fit plain logs and flat text.
Consider this fragment: {"msg":"status code: 500","code":0}. A text search for code matches inside the message string as well as the actual code key. jq '.code' selects the field structurally and returns 0.
JSON strings can contain commas, escaped quotes, newlines, braces, and nested objects. Text matching does not understand those structural boundaries.
| Approach | Good for | Weak for |
|---|---|---|
jq |
Nested objects, arrays, null, escaped strings |
Plain unstructured logs |
grep / sed |
Fixed markers, quick probes | String boundaries in JSON |
awk |
Columnar text, some delimiter files | Quoted commas and nesting |
Parse structured JSON with jq, then pass extracted text or numbers to other shell commands when needed.
Practical API Example
Fetch JSON from an API and read several fields from one response:
URL='https://httpbin.org/json'
response=$(curl -fsS "$URL") || exit 1
title=$(jq -r '.slideshow.title' <<<"$response")
author=$(jq -r '.slideshow.author' <<<"$response")
echo "title=$title author=$author"Build a POST body from local config, then send it with curl:
CONFIG=app.json
PORT=$(jq -er '.services[] | select(.name == "api") | .port' "$CONFIG") || {
echo "API port missing" >&2
exit 1
}
BODY=$(jq -n --arg app deploy-kit --argjson port "$PORT" \
'{application: $app, listen_port: $port}')
curl -fsS -X POST https://example.com/deploy \
-H 'Content-Type: application/json' \
--data-binary "$BODY"Do not print request bodies containing passwords or tokens. Log only the non-sensitive fields needed for troubleshooting.
Common jq / Bash Mistakes
| Mistake | Fix |
|---|---|
for x in $(jq ...) |
Use jq -c with while IFS= read -r |
jq -e . to validate syntax |
Use jq empty — jq -e treats valid false/null as failure |
// when false is meaningful |
Test the field explicitly; // treats false like null |
Shell variables inside the jq filter |
Use --arg and --argjson |
Parsing JSON with grep / sed |
Parse structured JSON with jq first |
Ignoring curl HTTP failures |
Use curl -fsS before piping to jq; still validate the body |
Summary
Use jq to parse JSON structure and -r when Bash needs a plain string. Dot paths access nested fields, [] iterates arrays, and select() filters records. Use --arg and --argjson instead of embedding shell values directly in jq filters.
In scripts, distinguish missing keys, null, empty strings, and Boolean false. Use jq -e when a query result should control Bash flow, and jq empty when you only need to validate JSON syntax.
References
- jq 1.7 manual
- jq download and installation
- RFC 8259 — The JavaScript Object Notation (JSON) Data Interchange Format
Frequently Asked Questions
Why does jq print quotes around my value?
jq outputs JSON by default. A string value is written as "alice" with quotes. Add -r (raw output) when you need plain text for a Bash variable.
What is jq -r?
-r (raw output) prints string values without JSON quotes. Use it for usernames, paths, and other text the shell passes to commands. Do not confuse -r with -R, which treats input as raw text instead of parsing JSON.
How do I loop over a JSON array in Bash?
Emit one compact object per line with jq -c '.array[]' file.json, then while IFS= read -r obj; do ... done < <(jq -c ...). Avoid for x in $(jq ...).
How do I handle null or a missing key?
A simple lookup returns null for both cases. Use has("key") to test whether a key exists. Use // default for fallbacks, but remember // also replaces false. JSON null, a missing key, and an empty string "" are different cases.
How do I pass Bash variables into jq safely?
Use --arg name "$value" for strings and --argjson name "$json" for JSON literals. Do not embed shell variables inside the jq filter source. Build new JSON with jq -n --arg when constructing request bodies.
How do I validate JSON in Bash?
Use jq empty file.json to check syntax. Use jq -e when a query result should affect script control flow, not to validate that a file is well-formed JSON.

