If you work with APIs, logs, or config files, you work with JSON. And jq is how you make that not painful. It’s like sed and awk had a baby that speaks JSON fluently.

The Basics

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Pretty-print JSON
curl -s https://api.example.com/data | jq .

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

# Get raw string (no quotes)
echo '{"name": "Alice"}' | jq -r '.name'
# Alice
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Nested fields
echo '{"user": {"name": "Alice", "email": "alice@example.com"}}' | jq '.user.name'
# "Alice"

# Multiple fields
echo '{"name": "Alice", "age": 30, "city": "NYC"}' | jq '{name, city}'
# {"name": "Alice", "city": "NYC"}

# Rename fields
echo '{"firstName": "Alice"}' | jq '{name: .firstName}'
# {"name": "Alice"}

Working with Arrays

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

# Get last element
echo '[1, 2, 3]' | jq '.[-1]'
# 3

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

# Get all elements
echo '[{"name": "Alice"}, {"name": "Bob"}]' | jq '.[].name'
# "Alice"
# "Bob"

# Wrap results in array
echo '[{"name": "Alice"}, {"name": "Bob"}]' | jq '[.[].name]'
# ["Alice", "Bob"]

Filtering

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# Select where condition is true
echo '[{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}]' | \
  jq '.[] | select(.age > 26)'
# {"name": "Alice", "age": 30}

# Multiple conditions
echo '[{"name": "Alice", "active": true}, {"name": "Bob", "active": false}]' | \
  jq '.[] | select(.active == true and .name != "Admin")'

# Contains
echo '[{"tags": ["dev", "prod"]}, {"tags": ["staging"]}]' | \
  jq '.[] | select(.tags | contains(["prod"]))'

# String matching
echo '[{"name": "Alice"}, {"name": "Bob"}, {"name": "Alicia"}]' | \
  jq '.[] | select(.name | startswith("Ali"))'

Transforming Data

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# Map over array
echo '[1, 2, 3]' | jq 'map(. * 2)'
# [2, 4, 6]

# Map with objects
echo '[{"name": "Alice", "score": 85}, {"name": "Bob", "score": 92}]' | \
  jq 'map({name, passed: .score >= 90})'
# [{"name": "Alice", "passed": false}, {"name": "Bob", "passed": true}]

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

# Update fields
echo '{"name": "alice"}' | jq '.name |= ascii_upcase'
# {"name": "ALICE"}

# Delete fields
echo '{"name": "Alice", "password": "secret"}' | jq 'del(.password)'
# {"name": "Alice"}

Aggregation

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# Length
echo '[1, 2, 3, 4, 5]' | jq 'length'
# 5

# Sum
echo '[1, 2, 3, 4, 5]' | jq 'add'
# 15

# Min/Max
echo '[3, 1, 4, 1, 5]' | jq 'min, max'
# 1
# 5

# Unique
echo '[1, 2, 2, 3, 3, 3]' | jq 'unique'
# [1, 2, 3]

# Group by
echo '[{"type": "a", "val": 1}, {"type": "b", "val": 2}, {"type": "a", "val": 3}]' | \
  jq 'group_by(.type) | map({type: .[0].type, total: map(.val) | add})'
# [{"type": "a", "total": 4}, {"type": "b", "total": 2}]

# Sort
echo '[{"name": "Bob"}, {"name": "Alice"}]' | jq 'sort_by(.name)'
# [{"name": "Alice"}, {"name": "Bob"}]

String Operations

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# Split
echo '{"path": "/usr/local/bin"}' | jq '.path | split("/")'
# ["", "usr", "local", "bin"]

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

# Replace
echo '{"msg": "Hello World"}' | jq '.msg | gsub("World"; "jq")'
# "Hello jq"

# String interpolation
echo '{"name": "Alice", "age": 30}' | jq '"Name: \(.name), Age: \(.age)"'
# "Name: Alice, Age: 30"

Working with Keys

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# Get keys
echo '{"a": 1, "b": 2, "c": 3}' | jq 'keys'
# ["a", "b", "c"]

# Get values
echo '{"a": 1, "b": 2, "c": 3}' | jq 'values' 
# Doesn't exist - use: [.[]]

# Convert object to array of key-value pairs
echo '{"a": 1, "b": 2}' | jq 'to_entries'
# [{"key": "a", "value": 1}, {"key": "b", "value": 2}]

# Convert back
echo '[{"key": "a", "value": 1}]' | jq 'from_entries'
# {"a": 1}

# Transform keys
echo '{"firstName": "Alice", "lastName": "Smith"}' | \
  jq 'with_entries(.key |= ascii_downcase)'
# {"firstname": "Alice", "lastname": "Smith"}

Conditionals

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# If-then-else
echo '{"status": 200}' | jq 'if .status == 200 then "OK" else "Error" end'
# "OK"

# Alternative operator (default value)
echo '{"name": "Alice"}' | jq '.age // 0'
# 0

echo '{"name": "Alice", "age": 30}' | jq '.age // 0'
# 30

# Try (ignore errors)
echo '{"data": "not json"}' | jq '.data | try fromjson'
# null

Real-World Examples

Parse API Response

1
2
3
4
5
6
7
# Extract users from paginated API
curl -s 'https://api.example.com/users' | \
  jq '.data[] | {id, name: .attributes.name, email: .attributes.email}'

# Get specific fields as TSV
curl -s 'https://api.example.com/users' | \
  jq -r '.data[] | [.id, .attributes.name] | @tsv'

Process Log Files

1
2
3
4
5
6
7
8
# Parse JSON logs, filter errors
cat app.log | jq -c 'select(.level == "error")' 

# Count by status code
cat access.log | jq -s 'group_by(.status) | map({status: .[0].status, count: length})'

# Get unique IPs
cat access.log | jq -s '[.[].ip] | unique'

Transform Config Files

1
2
3
4
5
6
7
8
# Merge configs
jq -s '.[0] * .[1]' base.json override.json

# Update nested value
jq '.database.host = "newhost.example.com"' config.json

# Add to array
jq '.allowed_ips += ["10.0.0.5"]' config.json

AWS CLI Output

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# List EC2 instance IDs and states
aws ec2 describe-instances | \
  jq -r '.Reservations[].Instances[] | [.InstanceId, .State.Name] | @tsv'

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

# Format as table
aws ec2 describe-instances | \
  jq -r '["ID", "Type", "State"], (.Reservations[].Instances[] | [.InstanceId, .InstanceType, .State.Name]) | @tsv' | \
  column -t

Kubernetes

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Get pod names and statuses
kubectl get pods -o json | \
  jq -r '.items[] | [.metadata.name, .status.phase] | @tsv'

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

# Get container images
kubectl get pods -o json | \
  jq -r '[.items[].spec.containers[].image] | unique | .[]'

Output Formats

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

# Tab-separated
echo '[{"a": 1, "b": 2}]' | jq -r '.[] | [.a, .b] | @tsv'
# 1	2

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

# URI encode
echo '{"q": "hello world"}' | jq -r '.q | @uri'
# hello%20world

# Base64
echo '{"data": "hello"}' | jq -r '.data | @base64'
# aGVsbG8=

Quick Reference

PatternDescription
.fieldGet field
.[]Iterate array
.[0]First element
.[-1]Last element
select(cond)Filter
map(expr)Transform array
. + {}Add fields
del(.field)Remove field
// defaultDefault value
@tsv, @csvOutput format
-rRaw output
-cCompact output
-sSlurp (read all input as array)

jq has a learning curve, but it pays off quickly. Once you internalize the patterns, you’ll wonder how you ever worked with JSON without it. Start with .field, .[].field, and select() — those three cover 80% of use cases.