Parsing JSON Safely with jq
JSON is structured data, so Bash should not treat it as decorated text. `jq` keeps JSON syntax and types intact while Bash focuses on orchestration and process control.
Learning objectives
By the end of this lesson
- Extract scalar JSON values safely.
- Filter arrays and objects with jq.
- Pass Bash values into jq without source injection.
- Validate JSON with jq exit status.
- Delay flattening until Bash needs simple records.
1. JSON should be parsed as JSON
JSON is structured data with strings, numbers, booleans, arrays, objects, null values, escaping, and nested structure. grep, cut, and regular expressions do not understand those semantics.
For Bash workflows, jq is the standard tool for selecting, transforming, validating, and generating JSON.
2. The dot filter selects a field
printf '%s\n' '{"service":"api","replicas":3}' |
jq '.service'By default, jq emits valid JSON. A JSON string therefore includes quotes.
3. Use -r when Bash needs a raw string
service=$(
printf '%s\n' '{"service":"api"}' |
jq -r '.service'
)
printf 'service=%s\n' "$service"-r removes JSON string quoting and escaping for scalar string output.
4. Iterate JSON arrays inside jq
printf '%s\n' \
'{"services":[{"name":"api"},{"name":"worker"}]}' |
jq -r '.services[].name'Each selected item becomes one output record. Decide whether newline is an acceptable downstream delimiter before feeding arbitrary strings into Bash line loops.
5. Filter objects with select()
jq -r '
.services[]
| select(.enabled == true)
| .name
' inventory.json6. Missing and null values need explicit policy
region=$(
jq -r '.region // "eu-central-1"' config.json
)// provides an alternate value for false/null-style cases. Be precise when false itself is meaningful and should not fall through.
7. JSON types should not be silently flattened
jq '
{
service: .service,
replicas: (.replicas | tonumber),
enabled: .enabled
}
' config.jsonIf Bash needs typed logic, either validate with jq before extraction or keep more of the transformation inside jq.
8. Pass Bash values into jq with --arg and --argjson
jq -n \
--arg service "$service" \
--argjson replicas "$replicas" \
'{
service: $service,
replicas: $replicas
}'Use --arg for strings and --argjson only for data that is already valid JSON.
9. jq exit status can participate in validation
if jq -e '.enabled == true' config.json >/dev/null; then
printf 'enabled\n'
else
status=$?
printf 'not enabled or jq failed status=%d\n' "$status" >&2
fi-e makes selected false/null results influence exit status, which is useful for predicates.
10. Compact output is useful for JSON-per-line streams
jq -c '.services[]' inventory.json-c emits one compact JSON value per result. This is a good boundary when downstream tools still need structured JSON rather than raw fields.
11. Convert to TSV only when field contracts are controlled
jq -r '
.services[]
| [.name, .replicas]
| @tsv
' inventory.json |
while IFS=$'\t' read -r name replicas; do
printf 'service=%s replicas=%s\n' "$name" "$replicas"
doneThis is convenient for simple scalar fields. For arbitrary values containing tabs/newlines, keep JSON structured longer or use a more explicit transport format.
12. Hands-on lab: query deployment inventory
mkdir -p "$HOME/devops-academy/bash/chapter13/lesson02"
cd "$HOME/devops-academy/bash/chapter13/lesson02"
cat > inventory.json <<'EOF'
{
"environment": "prod",
"services": [
{"name": "api", "replicas": 3, "enabled": true},
{"name": "worker", "replicas": 2, "enabled": true},
{"name": "legacy", "replicas": 1, "enabled": false}
]
}
EOF
command -v jq >/dev/null 2>&1 || exit 69
environment=$(jq -r '.environment' inventory.json)
printf 'environment=%s\n' "$environment"
jq -r '
.services[]
| select(.enabled == true)
| [.name, .replicas]
| @tsv
' inventory.json |
while IFS=$'\t' read -r name replicas; do
printf 'DEPLOY service=%s replicas=%s\n' "$name" "$replicas"
done
jq -e '
all(.services[]; (.replicas | type) == "number")
' inventory.json >/dev/nullVerification checklist
13. Knowledge check
Question 1. Why use jq -r for a Bash scalar?
Question 2. How should Bash strings be passed into jq?
--arg.Question 3. What does jq -e help with?
Question 4. Should nested JSON be parsed with grep or sed?
14. Summary
jq keeps JSON semantics intact while Bash handles orchestration. Use filters for structure, -r only when raw strings are required, pass shell data with --arg/--argjson, and keep complex transformations inside jq rather than flattening early.
15. Further reading
- jq Manual — filters, arrays, objects,
select,--arg,-e. - JSON specification and data model.
- ShellCheck documentation — command substitution and quoting.
- curl documentation — JSON API workflows.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this address.