jq Complete Guide: Command-Line JSON Processing
Every modern API speaks JSON. Kubernetes, Docker, AWS CLI, GitHub, Terraform, npm — they all output JSON. When you need to extract a value, filter a list, reshape a payload, or transform data on the command line, jq is the tool that makes it effortless. It is to JSON what sed, awk, and grep are to plain text.
This guide takes you from zero to production-ready jq usage. Every example shows the input JSON, the jq filter, and the exact output. By the end, you will be able to dismantle any JSON structure the command line throws at you.
Table of Contents
- Installation
- Basic Filters
- Array Operations
- The Pipe Operator
- Filtering with select()
- Transforming with map()
- Object Construction
- String Interpolation and Formatting
- Conditionals and Defaults
- reduce and group_by
- Output Formatters: @csv, @tsv, @base64
- Working with APIs: curl | jq
- Real-World Examples
- jq vs. Alternatives
- FAQ
1. Installation
jq is a single static binary with no dependencies. Install it with your system package manager:
# Ubuntu / Debian
sudo apt install jq
# macOS (Homebrew)
brew install jq
# Fedora / RHEL
sudo dnf install jq
# Arch Linux
sudo pacman -S jq
# Windows (Chocolatey)
choco install jq
# Windows (Scoop)
scoop install jq
Verify the installation:
$ jq --version
jq-1.7.1
2. Basic Filters
The identity filter . outputs the input unchanged but pretty-printed. Field access uses .fieldname, and nested fields chain with dots.
Sample input (saved as server.json):
{
"hostname": "web-01",
"ip": "10.0.1.42",
"cpu": { "cores": 8, "usage": 62.5 },
"tags": ["production", "us-east-1", "nginx"]
}
Pretty print the whole object:
$ cat server.json | jq '.'
{
"hostname": "web-01",
"ip": "10.0.1.42",
"cpu": { "cores": 8, "usage": 62.5 },
"tags": ["production", "us-east-1", "nginx"]
}
Extract a single field:
$ cat server.json | jq '.hostname'
"web-01"
Access nested fields:
$ cat server.json | jq '.cpu.cores'
8
Raw output (no quotes) with -r:
$ cat server.json | jq -r '.hostname'
web-01
3. Array Operations
Use .[] to iterate all elements, .[N] for a specific index, and .[N:M] for slicing.
Sample input (services.json):
[
{ "name": "api", "port": 8080, "status": "running" },
{ "name": "worker", "port": 9090, "status": "stopped" },
{ "name": "cache", "port": 6379, "status": "running" },
{ "name": "db", "port": 5432, "status": "running" }
]
Iterate and extract names:
$ cat services.json | jq '.[].name'
"api"
"worker"
"cache"
"db"
Get the first element:
$ cat services.json | jq '.[0]'
{ "name": "api", "port": 8080, "status": "running" }
Slice the first two:
$ cat services.json | jq '.[0:2]'
[
{ "name": "api", "port": 8080, "status": "running" },
{ "name": "worker", "port": 9090, "status": "stopped" }
]
Get the last element:
$ cat services.json | jq '.[-1]'
{ "name": "db", "port": 5432, "status": "running" }
Array length:
$ cat services.json | jq 'length'
4
4. The Pipe Operator
The | operator passes the output of one filter as input to the next, just like Unix pipes. This is the key to building complex transformations.
# Get names of all services, one per line
$ cat services.json | jq '.[] | .name'
"api"
"worker"
"cache"
"db"
# Get the port of the first service
$ cat services.json | jq '.[0] | .port'
8080
Pipes chain as deeply as needed:
$ cat server.json | jq '.tags | .[] | ascii_upcase'
"PRODUCTION"
"US-EAST-1"
"NGINX"
5. Filtering with select()
select(condition) keeps only values where the condition is true. It is jq's equivalent of SQL's WHERE clause.
# Only running services
$ cat services.json | jq '.[] | select(.status == "running")'
{ "name": "api", "port": 8080, "status": "running" }
{ "name": "cache", "port": 6379, "status": "running" }
{ "name": "db", "port": 5432, "status": "running" }
# Services on ports above 8000
$ cat services.json | jq '.[] | select(.port > 8000) | .name'
"api"
"worker"
# Combine conditions with and/or
$ cat services.json | jq '.[] | select(.status == "running" and .port < 7000)'
{ "name": "cache", "port": 6379, "status": "running" }
{ "name": "db", "port": 5432, "status": "running" }
String matching with test():
# Names containing "a"
$ cat services.json | jq '.[] | select(.name | test("a")) | .name'
"api"
"cache"
Wrap results back into an array with [...]:
$ cat services.json | jq '[.[] | select(.status == "running")]'
[
{ "name": "api", "port": 8080, "status": "running" },
{ "name": "cache", "port": 6379, "status": "running" },
{ "name": "db", "port": 5432, "status": "running" }
]
6. Transforming with map()
map(f) applies a filter to every element of an array and returns a new array. It is shorthand for [.[] | f].
# Extract just the names
$ cat services.json | jq 'map(.name)'
["api", "worker", "cache", "db"]
# Add a field to every object
$ cat services.json | jq 'map(. + {"env": "production"})'
[
{ "name": "api", "port": 8080, "status": "running", "env": "production" },
{ "name": "worker", "port": 9090, "status": "stopped", "env": "production" },
...
]
map_values() transforms values in an object (not an array):
$ echo '{"a": 1, "b": 2, "c": 3}' | jq 'map_values(. * 10)'
{ "a": 10, "b": 20, "c": 30 }
Combine map and select:
# Names of running services only
$ cat services.json | jq 'map(select(.status == "running")) | map(.name)'
["api", "cache", "db"]
7. Object Construction
Build new objects by wrapping field expressions in {}. This is how you reshape JSON into exactly the format you need.
# Reshape each service into a simpler object
$ cat services.json | jq '.[] | {service: .name, listening_on: .port}'
{ "service": "api", "listening_on": 8080 }
{ "service": "worker", "listening_on": 9090 }
{ "service": "cache", "listening_on": 6379 }
{ "service": "db", "listening_on": 5432 }
Shorthand: when the key name matches the field name, just use the field name:
$ cat services.json | jq '.[] | {name, port}'
{ "name": "api", "port": 8080 }
{ "name": "worker", "port": 9090 }
...
Computed keys use parentheses:
$ cat services.json | jq '.[] | {(.name): .port}'
{ "api": 8080 }
{ "worker": 9090 }
{ "cache": 6379 }
{ "db": 5432 }
Merge objects with +:
$ echo '{"a": 1}' | jq '. + {"b": 2, "c": 3}'
{ "a": 1, "b": 2, "c": 3 }
Remove a key with del():
$ cat services.json | jq 'map(del(.status))'
[
{ "name": "api", "port": 8080 },
{ "name": "worker", "port": 9090 },
{ "name": "cache", "port": 6379 },
{ "name": "db", "port": 5432 }
]
8. String Interpolation and Formatting
Use \(expr) inside strings for interpolation:
$ cat services.json | jq -r '.[] | "\(.name) is \(.status) on port \(.port)"'
api is running on port 8080
worker is stopped on port 9090
cache is running on port 6379
db is running on port 5432
String functions:
# Split and join
$ echo '"hello-world-foo"' | jq 'split("-") | join("_")'
"hello_world_foo"
# Length
$ echo '"devtoolbox"' | jq 'length'
10
# Upper / lower case
$ echo '"Hello"' | jq 'ascii_downcase'
"hello"
# Starts/ends with
$ echo '"api-gateway"' | jq 'startswith("api")'
true
# ltrimstr / rtrimstr
$ echo '"prefix-data"' | jq 'ltrimstr("prefix-")'
"data"
9. Conditionals and Defaults
if-then-else:
$ cat services.json | jq '.[] | if .status == "running" then "\(.name): OK" else "\(.name): DOWN" end'
"api: OK"
"worker: DOWN"
"cache: OK"
"db: OK"
Alternative operator // provides defaults for null or false:
$ echo '{"name": "api", "version": null}' | jq '.version // "unknown"'
"unknown"
$ echo '{}' | jq '.missing_field // "default"'
"default"
Optional operator ? suppresses errors:
# Without ? this would error because .foo is not an object
$ echo '{"foo": "bar"}' | jq '.foo.baz?'
null
Type checking:
$ echo '42' | jq 'type'
"number"
$ echo '["a","b"]' | jq 'type'
"array"
# Filter by type
$ echo '[1, "two", 3, "four"]' | jq '[.[] | select(type == "number")]'
[1, 3]
10. reduce and group_by
group_by groups array elements by a key:
Sample input (logs.json):
[
{ "level": "error", "msg": "Connection refused", "service": "api" },
{ "level": "info", "msg": "Request completed", "service": "api" },
{ "level": "error", "msg": "Timeout", "service": "worker" },
{ "level": "info", "msg": "Job finished", "service": "worker" },
{ "level": "error", "msg": "Disk full", "service": "api" }
]
# Count errors per service
$ cat logs.json | jq '
[.[] | select(.level == "error")]
| group_by(.service)
| map({service: .[0].service, errors: length})
'
[
{ "service": "api", "errors": 2 },
{ "service": "worker", "errors": 1 }
]
reduce accumulates a result across array elements:
# Sum all ports
$ cat services.json | jq 'reduce .[] as $s (0; . + $s.port)'
29981
# Build a lookup map { name: port }
$ cat services.json | jq 'reduce .[] as $s ({}; . + {($s.name): $s.port})'
{ "api": 8080, "worker": 9090, "cache": 6379, "db": 5432 }
Sorting and uniqueness:
$ cat services.json | jq 'sort_by(.port) | map(.name)'
["db", "cache", "api", "worker"]
$ echo '["b","a","c","a","b"]' | jq 'unique'
["a", "b", "c"]
11. Output Formatters: @csv, @tsv, @base64
jq has built-in format strings for converting data to other formats.
CSV output:
$ cat services.json | jq -r '.[] | [.name, .port, .status] | @csv'
"api",8080,"running"
"worker",9090,"stopped"
"cache",6379,"running"
"db",5432,"running"
TSV output:
$ cat services.json | jq -r '.[] | [.name, .port, .status] | @tsv'
api 8080 running
worker 9090 stopped
cache 6379 running
db 5432 running
Add a header row:
$ cat services.json | jq -r '["NAME","PORT","STATUS"], (.[] | [.name, .port, .status]) | @tsv'
NAME PORT STATUS
api 8080 running
worker 9090 stopped
cache 6379 running
db 5432 running
Base64 encode/decode:
$ echo '"Hello, World!"' | jq '@base64'
"SGVsbG8sIFdvcmxkIQ=="
$ echo '"SGVsbG8sIFdvcmxkIQ=="' | jq '@base64d'
"Hello, World!"
URI encode:
$ echo '"hello world & foo=bar"' | jq '@uri'
"hello%20world%20%26%20foo%3Dbar"
12. Working with APIs: curl | jq
The most common real-world jq pattern is piping API responses from curl.
GitHub: list repository names for a user:
$ curl -s https://api.github.com/users/torvalds/repos | jq -r '.[].name'
linux
subsurface-for-dirk
test-tlb
libdc-for-dirk
GitHub: stars and language for each repo:
$ curl -s https://api.github.com/users/torvalds/repos \
| jq '.[] | {name, stars: .stargazers_count, language}'
JSONPlaceholder: get titles of first 3 posts:
$ curl -s https://jsonplaceholder.typicode.com/posts \
| jq -r '.[0:3] | .[].title'
sunt aut facere repellat provident...
qui est esse
ea molestias quasi exercitationem...
Error-safe pattern with exit codes:
# Exit with error if API returns an error field
$ curl -s https://api.example.com/status \
| jq -e '.healthy' > /dev/null || echo "Service is unhealthy!"
The -e flag makes jq exit with code 1 if the output is false or null — perfect for health checks in scripts.
13. Real-World Examples
Parse Kubernetes Output
# List all pod names and their status
$ kubectl get pods -o json | jq -r '
.items[] | "\(.metadata.name)\t\(.status.phase)"
'
api-deployment-7f8b9-xk2lp Running
worker-deployment-5c3d1-m9nq Running
cache-pod-redis Running
# Find pods that are not Running
$ kubectl get pods -o json | jq '
[.items[] | select(.status.phase != "Running")
| {name: .metadata.name, phase: .status.phase}]
'
# Get container images across all pods
$ kubectl get pods -o json | jq -r '
.items[].spec.containers[].image' | sort -u
nginx:1.25
redis:7-alpine
node:20-slim
Process GitHub API Responses
# List open PRs with author and title
$ gh pr list --json number,title,author --jq '
.[] | "#\(.number) \(.author.login): \(.title)"
'
#142 alice: Add retry logic to API client
#139 bob: Fix database connection pooling
# Get workflow run statuses
$ gh run list --json name,status,conclusion --jq '
.[] | "\(.name): \(.status) (\(.conclusion // "pending"))"
'
Filter AWS CLI JSON
# List EC2 instances with name and state
$ aws ec2 describe-instances | jq -r '
.Reservations[].Instances[] |
{
id: .InstanceId,
name: (.Tags[] | select(.Key == "Name") | .Value),
state: .State.Name,
type: .InstanceType
}
'
# Find all S3 buckets created this year
$ aws s3api list-buckets | jq '
[.Buckets[] | select(.CreationDate | startswith("2026"))]
| map(.Name)
'
Transform Log Files
# Parse JSON log lines and extract errors
$ cat app.log | jq -r 'select(.level == "ERROR") | "\(.timestamp) \(.message)"'
# Count log entries by level
$ cat app.log | jq -s 'group_by(.level) | map({level: .[0].level, count: length})'
[
{ "level": "ERROR", "count": 12 },
{ "level": "INFO", "count": 384 },
{ "level": "WARN", "count": 47 }
]
Docker Inspect
# Get IP addresses of all running containers
$ docker inspect $(docker ps -q) | jq -r '
.[] | "\(.Name): \(.NetworkSettings.Networks[].IPAddress)"
'
/nginx: 172.18.0.3
/redis: 172.18.0.2
package.json Manipulation
# List all dependencies with versions
$ jq -r '.dependencies | to_entries[] | "\(.key)@\(.value)"' package.json
express@^4.18.2
dotenv@^16.3.1
pg@^8.11.3
# Bump a version
$ jq '.version = "2.1.0"' package.json > tmp.json && mv tmp.json package.json
Terraform Output
# Extract output values from terraform
$ terraform output -json | jq -r 'to_entries[] | "\(.key) = \(.value.value)"'
api_url = https://api.example.com
db_host = db-prod-001.rds.amazonaws.com
vpc_id = vpc-0a1b2c3d4e5f
14. jq vs. Alternatives
| Tool | Language | Best For | Notes |
|---|---|---|---|
| jq | C | Pure JSON processing | The standard. Widest documentation and community. |
| gojq | Go | jq + YAML input | Supports YAML. Better Unicode. Slightly different edge cases. |
| jaq | Rust | Speed-critical pipelines | Faster than jq on large files. Stricter correctness. |
| yq | Go | YAML / XML / TOML | Mike Farah's yq. jq-like syntax for multiple formats. |
| fx | Go | Interactive exploration | TUI for browsing JSON. Great for exploration, not scripting. |
For pure JSON on the command line, jq is the right choice. It is pre-installed or easily available on every platform, has the most comprehensive documentation, and is the de facto standard that other tools are measured against.
Frequently Asked Questions
What is jq and why should I use it?
jq is a lightweight, command-line JSON processor written in C with zero runtime dependencies. It lets you slice, filter, map, and transform structured JSON data as easily as sed, awk, and grep let you work with text. It is essential for working with REST APIs, Kubernetes, Docker, AWS CLI, Terraform output, CI/CD pipelines, and any tool that produces JSON.
How do I install jq on my system?
On Ubuntu/Debian: sudo apt install jq. On macOS: brew install jq. On Fedora: sudo dnf install jq. On Windows: choco install jq or scoop install jq. You can also download pre-built binaries from the official GitHub releases page.
What is the difference between .[] and .foo[] in jq?
.[] iterates over all values in the top-level array or object. .foo[] first accesses the field named foo, then iterates over its values. For example, if the input is {"users": [{"name": "Alice"}, {"name": "Bob"}]}, then .users[] outputs each user object individually, and .users[].name outputs "Alice" and "Bob" on separate lines.
How do I handle null values in jq?
Use the alternative operator // to provide defaults: .name // "unknown" returns "unknown" if .name is null or false. Use the optional operator ? to suppress errors from missing fields: .foo? returns null silently instead of erroring. You can also use select(. != null) to filter out null values from arrays.
Can jq modify JSON files in place?
jq does not support in-place editing directly. The standard pattern is to write to a temporary file and then replace the original: jq '.version = "2.0"' package.json > tmp.json && mv tmp.json package.json. Alternatively, use sponge from the moreutils package. Never redirect output to the same file you are reading from, as this will truncate it before jq can read it.
What is the difference between jq, jaq, gojq, and yq?
jq is the original C implementation and the standard. gojq is a Go reimplementation that supports YAML input and has better Unicode handling. jaq is a Rust reimplementation focused on speed and correctness. yq (by Mike Farah) is a Go tool for YAML, JSON, XML, CSV, and TOML using jq-like syntax. For pure JSON work, jq is the most widely available and documented choice.
How do I use jq with curl to process API responses?
Pipe curl output directly into jq: curl -s https://api.example.com/data | jq '.results[] | {id, name}'. Use the -s (silent) flag with curl to suppress the progress bar. For APIs requiring authentication, add headers: curl -s -H 'Authorization: Bearer TOKEN' URL | jq '.[].email'. Use -e with jq to exit with a non-zero code if the result is null or false.
Related Resources
- jq Playground — test jq filters interactively in your browser
- JSON Complete Guide — master the JSON format that jq processes
- JSON Formatter — pretty-print, minify, and validate JSON
- Bash Scripting Guide — automate workflows that use jq