How to Parse JSON in Bash

How to Parse JSON in Bash

How to Parse JSON in Bash

The first time I had to pull a single field out of a JSON API response inside a Bash script, I tried to do it with grep and sed. It technically worked until the API changed the order of fields in the response and my regex broke silently. That experience taught me that Bash isn’t a JSON-native language, but with the right tool — jq — parsing JSON in shell scripts becomes not just possible, but genuinely pleasant. This article covers everything I’ve learned about parsing JSON in Bash, from quick one-liners to full automation pipelines.

Why JSON Parsing in Bash Is Tricky

Bash has no native understanding of structured data. Everything in Bash is a string, so a JSON object like {"name": "Alice", "age": 30} is, to Bash, just a plain line of text. That means naive tools like grep or awk can extract data that looks right in simple cases, but they break the moment:

That’s why the standard approach in the Bash world is to use a dedicated JSON parser: jq.

Installing jq

On most systems, jq is available directly from the package manager:

# Debian/Ubuntu
sudo apt-get install jq

# RHEL/CentOS/Fedora
sudo dnf install jq

# macOS (Homebrew)
brew install jq

Verify it installed correctly:

jq --version

Beginner Example: Extracting a Single Field

Suppose I have this JSON in a file called user.json:

{
  "name": "Alice",
  "age": 30,
  "email": "alice@example.com"
}

To extract the name field:

jq -r '.name' user.json

Output:

Alice

The -r flag tells jq to output raw strings instead of quoted JSON strings, so I get Alice instead of "Alice".

Parsing JSON from a Variable

Often the JSON isn’t in a file — it’s the output of a curl command stored in a variable:

#!/usr/bin/env bash

response='{"status": "ok", "data": {"id": 42, "name": "Widget"}}'

status=$(echo "$response" | jq -r '.status')
id=$(echo "$response" | jq -r '.data.id')
name=$(echo "$response" | jq -r '.data.name')

echo "Status: $status"
echo "ID: $id"
echo "Name: $name"

Output:

Status: ok
ID: 42
Name: Widget

Notice how .data.id uses dot notation to reach into a nested object — this is one of jq‘s biggest advantages over regex-based extraction.

Working with Arrays

Given this JSON:

{
  "users": [
    {"name": "Alice", "age": 30},
    {"name": "Bob", "age": 25}
  ]
}

I can loop through the array and extract each name:

#!/usr/bin/env bash

json=$(cat users.json)

echo "$json" | jq -r '.users[].name'

Output:

Alice
Bob

To loop through and process each user individually in a Bash loop:

#!/usr/bin/env bash

while IFS= read -r name; do
    echo "Processing user: $name"
done < <(jq -r '.users[].name' users.json)

I use < <(...) (process substitution) instead of piping into the while loop directly, because piping would run the loop in a subshell, which means any variables set inside the loop wouldn’t persist afterward.

How This Works Internally

Real-World Use Case: Parsing an API Response

Here’s a script I actually use to check the latest release version of a GitHub repository via its API:

#!/usr/bin/env bash
set -euo pipefail

REPO="cli/cli"

response=$(curl -s "https://api.github.com/repos/${REPO}/releases/latest")

version=$(echo "$response" | jq -r '.tag_name')
published=$(echo "$response" | jq -r '.published_at')
url=$(echo "$response" | jq -r '.html_url')

echo "Latest version of ${REPO}: ${version}"
echo "Published on: ${published}"
echo "Release page: ${url}"

Example output:

Latest version of cli/cli: v2.55.0
Published on: 2026-06-12T10:15:00Z
Release page: https://github.com/cli/cli/releases/tag/v2.55.0

Automation Example: Monitoring an API and Alerting on Change

I run this as a cron job to notify me if a service’s status changes:

#!/usr/bin/env bash
set -euo pipefail

STATUS_FILE="/tmp/last_status.txt"
API_URL="https://status.example.com/api/v2/status.json"

current_status=$(curl -s "$API_URL" | jq -r '.status.description')

if [ -f "$STATUS_FILE" ]; then
    last_status=$(cat "$STATUS_FILE")
    if [ "$current_status" != "$last_status" ]; then
        echo "Status changed from '$last_status' to '$current_status'" 
        # send a notification here (e.g. mail, Slack webhook)
    fi
fi

echo "$current_status" > "$STATUS_FILE"

Building JSON Output From Bash

Parsing isn’t the only direction — I frequently need to generate JSON from Bash variables, and jq handles that safely too, avoiding manual string concatenation that can break on special characters:

name="Alice"
age=30

jq -n --arg name "$name" --argjson age "$age" \
    '{name: $name, age: $age}'

Output:

{
  "name": "Alice",
  "age": 30
}

Using --arg and --argjson is safer than string-building JSON manually, because jq handles escaping quotes and special characters automatically.

Best Practices

Security Considerations

Optimization Tips

Troubleshooting

Common Mistakes to Avoid

FAQs

Do I have to use jq, or are there alternatives? jq is by far the most common and best-supported tool for this in shell scripts. Alternatives include python3 -c "import json,sys; ..." or yq (which also handles YAML), but jq remains the standard for pure JSON work in Bash.

Can jq handle deeply nested JSON? Yes, you can chain as many .field accessors as needed, and use [] for arrays at any depth, e.g. .data.items[].details.price.

What if a field might not exist? Use jq -r '.field // "default"' to provide a fallback value if the field is missing or null.

Can I filter or transform JSON, not just extract fields? Yes, jq supports filtering, mapping, sorting, and even generating brand-new JSON structures — it’s closer to a small functional programming language than a simple query tool.

Summary

Parsing JSON in Bash without a real parser is a recipe for fragile scripts that break the moment the data shape changes even slightly. jq solves this cleanly, letting you extract nested fields, iterate over arrays, and even build new JSON output, all while keeping your Bash scripts readable and safe from injection issues. Once jq became part of my regular toolkit, I stopped writing brittle regex-based JSON parsers entirely.

References

Exit mobile version