jq Complete Guide: Command-Line JSON Processing

February 12, 2026

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.

⚙ Related tools: Try filters interactively in the jq Playground, format raw JSON with the JSON Formatter, and keep our JSON Complete Guide open as a reference.

Table of Contents

  1. Installation
  2. Basic Filters
  3. Array Operations
  4. The Pipe Operator
  5. Filtering with select()
  6. Transforming with map()
  7. Object Construction
  8. String Interpolation and Formatting
  9. Conditionals and Defaults
  10. reduce and group_by
  11. Output Formatters: @csv, @tsv, @base64
  12. Working with APIs: curl | jq
  13. Real-World Examples
  14. jq vs. Alternatives
  15. 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

Related Resources

jq Playground
Test jq filters interactively in your browser
JSON Formatter
Pretty-print, minify, and validate JSON data
JSON Complete Guide
Master JSON syntax, data types, parsing, and best practices
Bash Scripting Guide
Automate workflows and build pipelines that use jq