Parse JSON in Bash with jq

Tested on RHEL 10.2 (Coughlan)
Package bash 5.2.26-6.el10
jq 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
Short on time?Beta Get the gist of this 13-minute read in seconds, then ask follow-up questions

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:

bash
name=$(jq -r '.name' data.json)
echo "$name"

For JSON already in a Bash variable:

bash
json='{"name":"alice","id":42}'
name=$(jq -r '.name' <<<"$json")
echo "$name"
output
alice

jq 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:

bash
jq --version
output
jq-1.7.1

Common 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:

bash
jq -r '.name' app.json

Bash variable or string:

bash
json='{"name":"alice","id":42}'
jq -r '.name' <<<"$json"

Command or API output:

bash
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:

bash
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": ""
}
EOF

Pretty-print the document to inspect its structure:

bash
jq '.' app.json
output
{
  "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:

bash
jq '.owner.username' app.json
output
"alice"

Without -r, jq prints JSON syntax and keeps quotes around strings. For Bash variables, paths, and command arguments, use -r (raw output):

bash
jq -r '.owner.username' app.json
output
alice

-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:

bash
jq -r '.owner.email' app.json
output
alice@example.com

Bracket 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:

bash
jq -r '[.owner.username, .owner.email] | @tsv' app.json
output
alice	alice@example.com

Work with JSON Arrays

Read one element by index:

bash
jq -r '.services[0].name' app.json
output
api

Emit every element:

bash
jq -r '.services[].name' app.json
output
api
worker
cache

Count items:

bash
jq '.services | length' app.json
output
3

map reshapes each element. Active service names in a JSON array:

bash
jq '.services | map(select(.active) | .name)' app.json
output
[
  "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:

bash
jq -c '.services[]' app.json
output
{"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:

bash
jq -c '.services[] | select(.active)' app.json
output
{"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:

bash
jq '.owner.phone' app.json
output
null
bash
jq '.services[1].port' app.json
output
null

Use has() to tell whether a key exists:

bash
jq '.owner | has("phone")' app.json
output
false
bash
jq '.services[1] | has("port")' app.json
output
true

A 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:

bash
jq -r '.owner.phone // "n/a"' app.json
output
n/a

// treats both null and false as absent for fallback purposes. Do not use // when false is a meaningful value you need to preserve:

bash
jq '.services[1].active // true' app.json
output
true

active exists and is explicitly false, but // still returns true.

The notes field exists but is an empty string, not missing and not null:

bash
jq -r 'if .notes == "" then "(empty string)" else .notes end' app.json
output
(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:

bash
jq -e '.active' app.json
output
true

When 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:

bash
jq -e 'any(.services[]; .active)' app.json >/dev/null
echo "exit=$?"
output
exit=0

Use 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:

bash
jq -e '.services[] | select(.name == "missing")' app.json > /dev/null
echo "exit=$?"
output
exit=4

Use 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:

bash
u=alice
jq -r --arg name "$u" '.owner.username | select(. == $name)' app.json
output
alice

Use --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:

bash
jq -n --arg svc api --argjson port 8080 '{service: $svc, port: $port}'
output
{
  "service": "api",
  "port": 8080
}

For a numeric port read from JSON, capture it in Bash first, then pass it as JSON:

bash
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}'
output
{
  "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.

bash
TOKEN=sec123
jq -n --arg token "$TOKEN" '{auth: {type: "bearer", token: $token}}'
output
{
  "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.

bash
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)
output
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:

bash
jq -r '.services[] | [.name, (.port // "n/a")] | @tsv' app.json
output
api	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:

bash
if jq empty app.json 2>/dev/null; then
  echo "valid JSON"
else
  echo "invalid JSON"
fi
output
valid JSON

Malformed input makes jq exit non-zero and print a parse error:

bash
printf '%s\n' '{bad json' | jq .
output
jq: parse error: Invalid numeric literal at line 1, column 5

With 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:

bash
jq -e '.active' app.json >/dev/null && echo "active is true"

Test whether a record exists:

bash
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:

bash
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:

bash
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:

bash
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


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.

Finished reading?Beta Get the key takeaways in seconds, then ask follow-up questions
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with 15+ years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)