jq Tutorial: Query and Transform JSON from the Command Line
jq is a lightweight command-line tool that lets you slice, filter, map and transform JSON data the same way sed and awk handle text. It comes pre-installed on most Linux distributions, runs on macOS and Windows, and is the fastest way to query an API response without opening a browser.
Installing jq
# macOS brew install jq # Ubuntu / Debian sudo apt-get install jq # Windows (winget) winget install jqlang.jq # Verify jq --version # jq-1.7.1
The Identity Filter — Your First jq Command
The simplest jq filter is ., which means "the whole input". It pretty-prints JSON just like our JSON Formatter:
echo '{"name":"Alice","age":30}' | jq '.'
# Output:
{
"name": "Alice",
"age": 30
}Selecting Fields
Use .fieldName to extract a single value:
echo '{"name":"Alice","age":30,"city":"London"}' | jq '.name'
# "Alice"
# Multiple fields — build a new object
echo '{"name":"Alice","age":30,"city":"London"}' | jq '{name: .name, city: .city}'
# { "name": "Alice", "city": "London" }Working with Arrays
Use .[] to iterate over an array, and .[0] to get a specific index:
# Sample data
export DATA='[
{"id":1,"name":"Alice","score":92},
{"id":2,"name":"Bob","score":78},
{"id":3,"name":"Carol","score":95}
]'
# Get all names
echo $DATA | jq '.[].name'
# "Alice"
# "Bob"
# "Carol"
# Get first item
echo $DATA | jq '.[0]'
# { "id": 1, "name": "Alice", "score": 92 }
# Get array length
echo $DATA | jq 'length'
# 3Filtering with select()
select() keeps only the items that match a condition — the equivalent of SQL's WHERE clause:
# Students who scored above 80
echo $DATA | jq '[.[] | select(.score > 80)]'
# [
# { "id": 1, "name": "Alice", "score": 92 },
# { "id": 3, "name": "Carol", "score": 95 }
# ]
# Exact string match
echo $DATA | jq '.[] | select(.name == "Bob")'
# { "id": 2, "name": "Bob", "score": 78 }Transforming Data with map()
map() applies a filter to every element and returns a new array — like JavaScript's Array.map():
# Extract just the names as an array
echo $DATA | jq '[.[] | .name]'
# ["Alice", "Bob", "Carol"]
# Shorthand with map()
echo $DATA | jq 'map(.name)'
# ["Alice", "Bob", "Carol"]
# Add a field to every object
echo $DATA | jq 'map(. + {passed: (.score >= 80)})'
# [
# { "id": 1, "name": "Alice", "score": 92, "passed": true },
# { "id": 2, "name": "Bob", "score": 78, "passed": false },
# { "id": 3, "name": "Carol", "score": 95, "passed": true }
# ]Piping Filters
Chain filters with | just like shell pipes. Output of one filter becomes input to the next:
# Get names of students who passed, sorted echo $DATA | jq '[.[] | select(.score >= 80) | .name] | sort' # ["Alice", "Carol"] # Average score echo $DATA | jq '[.[].score] | add / length' # 88.33333333333333
Real-World: Querying an API Response
jq shines when combined with curl. Here's querying the GitHub API for a repo's open issues:
curl -s https://api.github.com/repos/curl/curl/issues \
| jq '[.[] | {title: .title, user: .user.login, labels: [.labels[].name]}] | .[0:3]'Tip: Validate your JSON first
If jq throws parse error, the input isn't valid JSON. Paste it into the JSON Validator to find and fix the exact line before running jq.
Useful jq Flags
| Flag | What it does |
|---|---|
-r | Raw output — strips quotes from strings |
-c | Compact output — one line, no whitespace |
-e | Exit 1 if output is false/null (useful in scripts) |
-s | Slurp — read all inputs into a single array |
--arg k v | Pass a shell variable into jq as a string |
-f file.jq | Read filter from a file instead of inline |
Common jq Patterns Cheatsheet
# Keys of an object
echo '{"a":1,"b":2}' | jq 'keys' # ["a","b"]
# Check if key exists
echo '{"a":1}' | jq 'has("a")' # true
# Delete a key
echo '{"a":1,"b":2}' | jq 'del(.b)' # {"a":1}
# Flatten nested array
echo '[[1,2],[3,[4,5]]]' | jq 'flatten' # [1,2,3,4,5]
# Group by field
echo $DATA | jq 'group_by(.score > 80)'
# Sort by field
echo $DATA | jq 'sort_by(.score)'
# Min / max
echo $DATA | jq 'min_by(.score).name' # "Bob"
echo $DATA | jq 'max_by(.score).name' # "Carol"
# String interpolation
echo '{"name":"Alice"}' | jq -r '"Hello, (.name)!"'
# Hello, Alice!Optional object identifier
Use .field? instead of .field when the field might not exist. Without ?, accessing a missing field on null throws an error that stops the pipeline.
Try it in the Browser
Don't have jq installed yet? Paste your JSON into the JSON Formatter to pretty-print and inspect it, or use the JSON Validator to make sure your data is clean before piping it through jq filters.