Parse JSON in Bash: Shell Scripting with jq
Bash has no built-in JSON parser. The standard solution is jq — a lightweight tool that extracts values, filters arrays, and transforms JSON entirely in the terminal. With jq, you can assign JSON fields to bash variables, loop over JSON arrays, and process API responses in one-liners or full shell scripts.
Need to query JSON without installing anything? Try Jsonic's JSONPath Tester — paste your JSON and run JSONPath expressions directly in the browser.
Open JSONPath TesterInstall jq
jq is available in the package manager of every major Linux distribution and macOS. It is also pre-installed on many CI/CD environments including GitHub Actions runners and GitLab CI — you may already have it.
# macOS
brew install jq
# Ubuntu / Debian
sudo apt-get install jq
# Alpine (Docker)
apk add jq
# Check version
jq --version # jq-1.7.1Run jq --version to confirm the install. Any version from 1.6 onward supports all the patterns in this guide.
Extract a value into a bash variable
The most common task — get a field value from JSON and use it later in a script. Wrap the jq command in $() to capture the output into a variable.
JSON='{"name":"Alice","age":30,"city":"Paris"}'
# Assign a field to a variable
name=$(echo "$JSON" | jq -r '.name')
age=$(echo "$JSON" | jq -r '.age')
echo "Name: $name" # Name: Alice
echo "Age: $age" # Age: 30
# Nested field
city=$(echo "$JSON" | jq -r '.address.city // "unknown"')The -r flag stands for raw output: it strips the surrounding double quotes that jq adds to JSON strings. Without -r, a string value like Alice would be captured as "Alice" — with the quotes included, which breaks most shell comparisons and commands.
The // "fallback" syntax is jq's alternative operator. It returns the right-hand value whenever the left side evaluates to null or is absent. Use it to avoid empty variables when a field is optional.
Read JSON from a file
Pass the filename as the last argument to jq instead of piping from echo. This is cleaner and avoids an extra process for file-based workflows like reading config files.
# Read a top-level field from a file
name=$(jq -r '.name' config.json)
# Read nested fields
db_host=$(jq -r '.database.host' config.json)
db_port=$(jq -r '.database.port' config.json)
# Read multiple fields in one pass using string interpolation
jq -r '"(.host):(.port)"' config.json
# output: localhost:5432The string interpolation syntax "\(.field)" lets you combine multiple fields into a single output line, avoiding multiple jq invocations. This is useful when you need a formatted connection string or URL from a config file.
Loop over a JSON array
Processing each element in a JSON array is one of the most common shell scripting patterns. Use a while read loop with process substitution — it handles whitespace and special characters correctly.
# Loop over a JSON array of strings
TAGS='["frontend","backend","devops"]'
while IFS= read -r tag; do
echo "Processing tag: $tag"
done < <(echo "$TAGS" | jq -r '.[]')
# Loop over a JSON array of objects
USERS='[{"name":"Alice","email":"alice@example.com"},{"name":"Bob","email":"bob@example.com"}]'
while IFS= read -r user; do
name=$(echo "$user" | jq -r '.name')
email=$(echo "$user" | jq -r '.email')
echo "Sending email to $name at $email"
done < <(echo "$USERS" | jq -c '.[]')When iterating over objects, use the -c flag (compact output) instead of -r. This outputs each JSON object on a single line, so the while read loop receives one complete object per iteration. You can then pipe each line back through jq to extract individual fields.
Always prefer while IFS= read -r over for item in $(jq ...). The for form uses word splitting, which breaks whenever a value contains spaces, tabs, or newlines.
Process API responses
The curl + jq pipeline is the standard pattern for interacting with REST APIs in shell scripts. Use -s to silence curl's progress output so only JSON reaches jq.
# GET request: extract a field
response=$(curl -s "https://api.github.com/repos/jqlang/jq")
stars=$(echo "$response" | jq -r '.stargazers_count')
echo "jq has $stars stars on GitHub"
# POST request: send JSON body, extract response field
token=$(curl -s -X POST "https://api.example.com/auth" \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"secret"}' \
| jq -r '.token')
# Use the token in subsequent requests
curl -s -H "Authorization: Bearer $token" "https://api.example.com/users" \
| jq '.[] | {id, name}'Store the full response in a variable first (as shown above) when you need to extract multiple fields from a single request. This avoids making the same HTTP call twice. For single-field extractions, piping directly to jq is cleaner.
Error handling and validation
jq exits with a non-zero status code on invalid JSON and when a filter produces null (with -e). Use this in bash conditionals to validate input and handle missing fields gracefully.
# Check if jq parse succeeded (non-zero exit on invalid JSON)
if echo "$JSON" | jq . > /dev/null 2>&1; then
echo "Valid JSON"
else
echo "Invalid JSON" >&2
exit 1
fi
# Check if a field exists before using it
if echo "$JSON" | jq -e '.apiKey' > /dev/null 2>&1; then
api_key=$(echo "$JSON" | jq -r '.apiKey')
else
echo "Missing apiKey in config" >&2
exit 1
fi
# Handle null values with a fallback
timeout=$(echo "$JSON" | jq -r '.timeout // 30')The -e flag (exit status) makes jq exit with status 1 when the output value is null or false. This turns jq into a boolean test for field existence, letting you use it directly in bash if conditions without any string comparison.
Build JSON in bash
Use jq -n to construct JSON objects from bash variables. This is safer than string concatenation — jq handles escaping automatically so values with quotes, backslashes, or unicode never corrupt the output.
# Build a JSON object from bash variables
name="Alice"
age=30
email="alice@example.com"
json=$(jq -n \
--arg name "$name" \
--argjson age "$age" \
--arg email "$email" \
'{name: $name, age: $age, email: $email}')
echo "$json"
# {"name":"Alice","age":30,"email":"alice@example.com"}
# Append to a JSON array
new_item='{"id":4,"label":"new"}'
updated=$(echo "$existing_array" | jq --argjson item "$new_item" '. + [$item]')The three key flags for building JSON:
--arg name "$value"— injects a bash variable as a JSON string. Use for text fields. jq escapes special characters automatically.--argjson name "$value"— injects a bash variable as a raw JSON value. Use for numbers, booleans, arrays, or objects.jq -n— null input mode: no piped input is required. Use when constructing JSON from scratch rather than transforming an existing document.
For querying JSON without installing tools, try Jsonic's JSONPath Tester in the browser — paste any JSON and run JSONPath expressions instantly.
Open JSONPath TesterFrequently asked questions
How do I parse JSON in a bash script?
Install jq (brew install jq on macOS, apt-get install jq on Linux) and pipe JSON to it: value=$(echo "$json" | jq -r '.fieldName'). The -r flag outputs raw strings without surrounding quotes, which is what you want for bash variable assignment. For file input: value=$(jq -r '.fieldName' data.json). Avoid using grep or sed to parse JSON — they break on nested structures and special characters.
How do I extract a nested JSON value in bash?
Chain field selectors with dots: jq -r '.user.address.city' data.json. For array access: jq -r '.users[0].name' data.json. For optional or nullable fields, use the // operator to provide a fallback: jq -r '.config.timeout // 30' data.json returns 30 if .config.timeout is null or absent. The // operator is jq's alternative operator — it is not a bash comment inside a jq filter string.
How do I loop over a JSON array in bash?
Use process substitution with jq: while IFS= read -r item; do echo "$item"; done < <(jq -c '.[]' data.json). The -c flag outputs each array element on one compact line. For arrays of strings, use -r instead. Avoid for item in $(jq ...) — word splitting breaks on whitespace in values.
How do I create a JSON object in bash with jq?
Use jq -n with --arg for strings and --argjson for numbers, booleans, or JSON values: jq -n --arg name "Alice" --argjson age 30 '{name: $name, age: $age}'. The -n flag means no input is required. Never build JSON by concatenating strings — unescaped quotes or special characters will produce invalid output.
Does bash have a built-in JSON parser?
No. Bash treats everything as text. The standard tool is jq, which is available in the package manager of every major Linux distribution and macOS (Homebrew). It is also pre-installed in GitHub Actions runners, many Docker base images, and cloud CLI environments. Alternatives include Python one-liners (python3 -c "import sys,json; print(json.load(sys.stdin)['key'])"), but jq is faster and avoids Python startup overhead for simple extractions.
How do I check if a JSON field exists in bash?
Use jq -e: it exits with status 1 if the output is null or false, enabling standard bash if-checks: if jq -e '.apiKey' config.json > /dev/null; then .... To check and extract in one pass: value=$(jq -r '.apiKey // empty' config.json) — the empty filter causes jq to output nothing and exit non-zero when the field is absent, so $value is empty and you can test with if [ -z "$value" ].