If you work with APIs, logs, or configuration files, you work with JSON. And if you work with JSON from the command line, jq is indispensable. Here are the patterns I use daily.

The Basics

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Pretty print
echo '{"name":"alice","age":30}' | jq '.'

# Extract a field
echo '{"name":"alice","age":30}' | jq '.name'
# "alice"

# Raw output (no quotes)
echo '{"name":"alice","age":30}' | jq -r '.name'
# alice

The -r flag is your friend. Use it whenever you want the actual value, not a JSON string.

Working with Arrays

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# Get all elements
echo '[1,2,3]' | jq '.[]'
# 1
# 2
# 3

# Get specific index
echo '["a","b","c"]' | jq '.[1]'
# "b"

# Get length
echo '[1,2,3,4,5]' | jq 'length'
# 5

# First and last
echo '[1,2,3,4,5]' | jq 'first'  # 1
echo '[1,2,3,4,5]' | jq 'last'   # 5

# Slice
echo '[0,1,2,3,4,5]' | jq '.[2:4]'
# [2, 3]

Extracting from Nested Structures

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
DATA='{"user":{"profile":{"email":"alice@example.com"}}}'

# Chain keys
echo $DATA | jq '.user.profile.email'
# "alice@example.com"

# Optional access (no error if missing)
echo '{}' | jq '.user.profile.email?'
# null

# With default
echo '{}' | jq '.user.profile.email // "no email"'
# "no email"

Filtering Arrays

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
USERS='[{"name":"alice","age":30},{"name":"bob","age":25},{"name":"carol","age":35}]'

# Select by condition
echo $USERS | jq '.[] | select(.age > 28)'
# {"name":"alice","age":30}
# {"name":"carol","age":35}

# Extract field from filtered results
echo $USERS | jq '[.[] | select(.age > 28) | .name]'
# ["alice", "carol"]

# Multiple conditions
echo $USERS | jq '.[] | select(.age > 25 and .name != "carol")'
# {"name":"alice","age":30}

Transforming Data

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# Build new objects
echo '{"first":"Alice","last":"Smith"}' | jq '{fullName: "\(.first) \(.last)"}'
# {"fullName": "Alice Smith"}

# Map over arrays
echo '[1,2,3]' | jq 'map(. * 2)'
# [2, 4, 6]

# Add fields
echo '{"name":"alice"}' | jq '. + {role: "admin"}'
# {"name":"alice","role":"admin"}

# Update fields
echo '{"name":"alice","age":30}' | jq '.age = .age + 1'
# {"name":"alice","age":31}

# Delete fields
echo '{"name":"alice","age":30,"temp":true}' | jq 'del(.temp)'
# {"name":"alice","age":30}

String Manipulation

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# Interpolation
echo '{"name":"alice"}' | jq '"Hello, \(.name)!"'
# "Hello, alice!"

# Split and join
echo '"a,b,c"' | jq 'split(",")'
# ["a", "b", "c"]

echo '["a","b","c"]' | jq 'join("-")'
# "a-b-c"

# Case conversion
echo '"Hello World"' | jq 'ascii_downcase'
# "hello world"

# Test with regex
echo '"user@example.com"' | jq 'test("@")'
# true

Aggregation

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
NUMBERS='[1,2,3,4,5]'

echo $NUMBERS | jq 'add'      # 15
echo $NUMBERS | jq 'min'      # 1
echo $NUMBERS | jq 'max'      # 5
echo $NUMBERS | jq 'add/length'  # 3 (average)

# Group by
ITEMS='[{"type":"fruit","name":"apple"},{"type":"veg","name":"carrot"},{"type":"fruit","name":"banana"}]'
echo $ITEMS | jq 'group_by(.type)'
# [[{"type":"fruit","name":"apple"},{"type":"fruit","name":"banana"}],[{"type":"veg","name":"carrot"}]]

# Count by type
echo $ITEMS | jq 'group_by(.type) | map({type: .[0].type, count: length})'
# [{"type":"fruit","count":2},{"type":"veg","count":1}]

Real-World API Examples

Parse AWS CLI Output

1
2
3
4
5
6
7
8
9
# List EC2 instance IDs and states
aws ec2 describe-instances | jq -r '.Reservations[].Instances[] | "\(.InstanceId)\t\(.State.Name)"'

# Get only running instances
aws ec2 describe-instances | jq '.Reservations[].Instances[] | select(.State.Name == "running")'

# Extract specific tags
aws ec2 describe-instances | jq -r '.Reservations[].Instances[] | 
  "\(.InstanceId)\t\(.Tags // [] | map(select(.Key == "Name")) | .[0].Value // "unnamed")"'

Parse Docker Output

1
2
3
4
5
# Container names and status
docker inspect $(docker ps -q) | jq -r '.[] | "\(.Name)\t\(.State.Status)"'

# Get container IPs
docker inspect $(docker ps -q) | jq -r '.[] | "\(.Name): \(.NetworkSettings.IPAddress)"'

Parse Kubernetes Output

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Pod names and status
kubectl get pods -o json | jq -r '.items[] | "\(.metadata.name)\t\(.status.phase)"'

# Pods not running
kubectl get pods -o json | jq '.items[] | select(.status.phase != "Running") | .metadata.name'

# Resource requests
kubectl get pods -o json | jq '.items[] | {
  name: .metadata.name,
  cpu: .spec.containers[0].resources.requests.cpu,
  memory: .spec.containers[0].resources.requests.memory
}'

Parse GitHub API

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# List repo names
curl -s "https://api.github.com/users/octocat/repos" | jq -r '.[].name'

# Stars and forks
curl -s "https://api.github.com/users/octocat/repos" | \
  jq -r '.[] | "\(.name): ⭐\(.stargazers_count) 🍴\(.forks_count)"'

# Most starred
curl -s "https://api.github.com/users/octocat/repos" | \
  jq -r 'sort_by(.stargazers_count) | reverse | .[0:5] | .[].name'

Multiple Outputs to Single Array

1
2
3
4
5
6
7
# Wrap multiple outputs in an array
echo '{"a":1,"b":2}' | jq '[.a, .b]'
# [1, 2]

# Collect filtered results
echo '[1,2,3,4,5]' | jq '[.[] | select(. > 2)]'
# [3, 4, 5]

Reading from Files

1
2
3
4
5
6
7
8
# From file
jq '.config.database' config.json

# Multiple files
jq -s '.' file1.json file2.json  # Slurp into array

# Combine objects from multiple files
jq -s 'add' defaults.json overrides.json

Outputting Different Formats

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# Compact (no whitespace)
echo '{"a": 1, "b": 2}' | jq -c '.'
# {"a":1,"b":2}

# Tab-separated (for further processing)
echo '[{"a":1,"b":2},{"a":3,"b":4}]' | jq -r '.[] | [.a, .b] | @tsv'
# 1	2
# 3	4

# CSV
echo '[{"a":1,"b":2},{"a":3,"b":4}]' | jq -r '.[] | [.a, .b] | @csv'
# 1,2
# 3,4

# URI encoding
echo '"hello world"' | jq -r '@uri'
# hello%20world

Error Handling

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Suppress errors
echo 'not json' | jq '.' 2>/dev/null || echo "Invalid JSON"

# Check if valid JSON
if echo "$DATA" | jq -e '.' >/dev/null 2>&1; then
  echo "Valid JSON"
fi

# The -e flag sets exit code based on output (null/false = 1)
echo 'null' | jq -e '.'  # exits 1
echo '1' | jq -e '.'     # exits 0

My .bashrc Helpers

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Pretty print JSON from clipboard
alias jqc='pbpaste | jq .'

# Format JSON file in place
jqf() { jq '.' "$1" > "$1.tmp" && mv "$1.tmp" "$1"; }

# Extract field from JSON stream (logs, etc.)
jqfield() { jq -r ".$1 // empty"; }

# Quick API response inspection
apiq() { curl -s "$1" | jq '.'; }

Quick Reference

TaskCommand
Pretty printjq '.'
Extract fieldjq '.fieldname'
Raw outputjq -r '.field'
Array elementsjq '.[]'
Filter arrayjq '.[] | select(.x > 5)'
Map arrayjq 'map(.field)'
Build objectjq '{new: .old}'
Countjq 'length'
Sortjq 'sort_by(.field)'
Uniquejq 'unique'
First Njq '.[0:5]'

jq has a learning curve, but it’s worth climbing. Once fluent, you’ll parse JSON faster than opening a browser to find an online formatter. The command line becomes your data transformation playground.

For deeper exploration, the official manual is comprehensive, and jqplay.org lets you experiment interactively.