Every API returns JSON. Every config file is JSON. If you’re not fluent in jq, you’re copying data by hand like it’s 1995.

The Basics

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

# Extract a field
echo '{"name":"test","value":42}' | jq '.name'
# "test"

# Raw output (no quotes)
echo '{"name":"test","value":42}' | jq -r '.name'
# test

Working with APIs

1
2
3
4
5
6
7
8
# GitHub API
curl -s https://api.github.com/users/torvalds | jq '.login, .public_repos'

# Extract specific fields
curl -s https://api.github.com/repos/stedolan/jq | jq '{name, stars: .stargazers_count, language}'

# AWS CLI (already outputs JSON)
aws ec2 describe-instances | jq '.Reservations[].Instances[] | {id: .InstanceId, state: .State.Name}'

Array Operations

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# Sample data
DATA='[{"name":"alice","age":30},{"name":"bob","age":25},{"name":"carol","age":35}]'

# First element
echo $DATA | jq '.[0]'

# Last element
echo $DATA | jq '.[-1]'

# Slice
echo $DATA | jq '.[0:2]'

# All names
echo $DATA | jq '.[].name'

# Array of names
echo $DATA | jq '[.[].name]'

# Length
echo $DATA | jq 'length'

Filtering

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Select by condition
echo $DATA | jq '.[] | select(.age > 28)'

# Multiple conditions
echo $DATA | jq '.[] | select(.age > 25 and .name != "carol")'

# Contains
echo '[{"tags":["web","api"]},{"tags":["cli"]}]' | jq '.[] | select(.tags | contains(["api"]))'

# Has key
echo '{"a":1,"b":null}' | jq 'has("a"), has("c")'

Transformation

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# Add/modify fields
echo '{"name":"test"}' | jq '. + {status: "active", count: 0}'

# Update existing field
echo '{"count":5}' | jq '.count += 1'

# Delete field
echo '{"a":1,"b":2,"c":3}' | jq 'del(.b)'

# Rename key
echo '{"old_name":"value"}' | jq '{new_name: .old_name}'

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

# Map with objects
echo $DATA | jq 'map({username: .name, birth_year: (2026 - .age)})'

String Operations

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# Concatenation
echo '{"first":"John","last":"Doe"}' | jq '.first + " " + .last'

# String interpolation
echo '{"name":"test","ver":"1.0"}' | jq '"\(.name)-\(.ver).tar.gz"'

# Split
echo '{"path":"/usr/local/bin"}' | jq '.path | split("/")'

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

# Upper/lower
echo '"Hello World"' | jq 'ascii_downcase'
echo '"Hello World"' | jq 'ascii_upcase'

# Test regex
echo '{"email":"test@example.com"}' | jq '.email | test("@")'

# Replace
echo '"hello world"' | jq 'gsub("world"; "jq")'

Conditionals

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

# Alternative operator (default value)
echo '{"a":1}' | jq '.b // "default"'

# Null handling
echo '{"a":null}' | jq '.a // "was null"'

# Error handling
echo '{}' | jq '.missing.nested // "not found"'

Grouping and 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
26
27
28
29
30
LOGS='[
  {"level":"error","msg":"failed"},
  {"level":"info","msg":"started"},
  {"level":"error","msg":"timeout"},
  {"level":"info","msg":"completed"}
]'

# Group by field
echo $LOGS | jq 'group_by(.level)'

# Count per group
echo $LOGS | jq 'group_by(.level) | map({level: .[0].level, count: length})'

# Unique values
echo $LOGS | jq '[.[].level] | unique'

# Sort
echo $DATA | jq 'sort_by(.age)'

# Reverse sort
echo $DATA | jq 'sort_by(.age) | reverse'

# Min/max
echo '[5,2,8,1,9]' | jq 'min, max'

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

# Average
echo '[1,2,3,4,5]' | jq 'add / length'

Constructing Output

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Build new object
curl -s https://api.github.com/users/torvalds | jq '{
  username: .login,
  repos: .public_repos,
  profile: .html_url
}'

# Build array
echo '{"users":[{"name":"a"},{"name":"b"}]}' | jq '[.users[].name]'

# Multiple outputs to array
echo '{"a":1,"b":2}' | jq '[.a, .b, .a + .b]'

# Key-value pairs
echo '{"a":1,"b":2}' | jq 'to_entries'
# [{"key":"a","value":1},{"key":"b","value":2}]

# Back to object
echo '[{"key":"a","value":1}]' | jq 'from_entries'

# Transform keys
echo '{"old_a":1,"old_b":2}' | jq 'with_entries(.key |= ltrimstr("old_"))'

Real-World Examples

Parse AWS Instance List

1
2
3
4
5
aws ec2 describe-instances | jq -r '
  .Reservations[].Instances[] |
  [.InstanceId, .State.Name, (.Tags[]? | select(.Key=="Name") | .Value) // "unnamed"] |
  @tsv
'

Filter Docker Containers

1
2
3
4
5
6
docker inspect $(docker ps -q) | jq '.[] | {
  name: .Name,
  image: .Config.Image,
  status: .State.Status,
  ip: .NetworkSettings.IPAddress
}'

Process Log Files

1
2
3
4
5
6
7
# Count errors by type
cat app.log | jq -s 'group_by(.error_type) | map({type: .[0].error_type, count: length}) | sort_by(.count) | reverse'

# Extract errors from last hour
cat app.log | jq --arg cutoff "$(date -d '1 hour ago' -Iseconds)" '
  select(.timestamp > $cutoff and .level == "error")
'

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

Generate Reports

1
2
3
4
5
6
# Kubernetes pod status
kubectl get pods -o json | jq -r '
  .items[] |
  [.metadata.name, .status.phase, (.status.containerStatuses[0].restartCount // 0)] |
  @tsv
' | column -t

Useful Flags

 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
26
# Compact output (no pretty print)
jq -c '.'

# Raw output (no quotes on strings)
jq -r '.name'

# Raw input (treat input as string, not JSON)
jq -R 'split(",")'

# Slurp (read all inputs into array)
cat *.json | jq -s '.'

# Pass variable
jq --arg name "test" '.name = $name'

# Pass JSON variable
jq --argjson count 42 '.count = $count'

# Read from file
jq --slurpfile users users.json '.users = $users'

# Exit with error if output is null/false
jq -e '.important_field' && echo "exists"

# Sort keys in output
jq -S '.'

Output Formats

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Tab-separated
echo $DATA | jq -r '.[] | [.name, .age] | @tsv'

# CSV
echo $DATA | jq -r '.[] | [.name, .age] | @csv'

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

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

# Shell-safe
echo '{"cmd":"echo hello"}' | jq -r '.cmd | @sh'

Debugging

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Show type
echo '{"a":[1,2,3]}' | jq '.a | type'

# Show keys
echo '{"a":1,"b":2}' | jq 'keys'

# Debug output (shows intermediate values)
echo '{"x":{"y":{"z":1}}}' | jq '.x | debug | .y | debug | .z'

# Path to value
echo '{"a":{"b":{"c":1}}}' | jq 'path(.. | select(. == 1))'

Quick Reference

 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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# Identity
.

# Field access
.field
.field.nested

# Array access
.[0]
.[-1]
.[2:5]

# Iterate array
.[]

# Pipe
.[] | .name

# Collect into array
[.[] | .name]

# Object construction
{newkey: .oldkey}

# Conditionals
if COND then A else B end
VALUE // DEFAULT

# Comparison
==, !=, <, >, <=, >=
and, or, not

# Array functions
map(f), select(f), sort_by(f), group_by(f), unique, length, first, last, nth(n), flatten, reverse, contains(x), inside(x), add, min, max

# String functions
split(s), join(s), test(re), match(re), gsub(re;s), ascii_downcase, ascii_upcase, ltrimstr(s), rtrimstr(s), startswith(s), endswith(s)

# Object functions
keys, values, has(k), in(o), to_entries, from_entries, with_entries(f)

# Type functions
type, isnumber, isstring, isnull, isboolean, isarray, isobject

jq turns JSON from a data format into a query language. Once you internalize the pipe-and-filter model, you’ll wonder how you ever survived without it.

Start with simple extractions. Build up to transformations. Before long, you’ll be writing one-liners that replace entire scripts.