cheat sheet
jq
Slice, filter, map, and transform JSON data from the command line. Covers all essential filters, built-in functions, select, map, reduce, streaming, jq 1.7/1.8 additions, and real-world API response processing.
jq — JSON Processor
What it is
jq is a free, open-source command-line JSON processor written in C and maintained at jqlang.org. It provides a lightweight functional language for slicing, filtering, mapping, and transforming JSON data from files or stdin, with no runtime dependencies beyond the binary itself. Reach for jq whenever you need to extract fields from API responses, reshape JSON for other tools, or pretty-print raw JSON in a shell pipeline; for very large JSON streams, consider jq's streaming mode or a tool like fx for interactive exploration.
The project ended a five-year release gap with jq 1.7 (Sept 2023), followed by 1.7.1 (Dec 2023) and 1.8.0 (June 2024). Recent releases brought pick, INDEX/IN/JOIN, trim/ltrim/rtrim, abs, toboolean, --raw-output0, JQ_COLORS/NO_COLOR, and switched string indexing builtins from bytes to Unicode code points — see What's new in jq 1.7 / 1.8 below.
Installation
sudo apt install jq
sudo dnf install jq
brew install jq
Output: (none — exits 0 on success)
Syntax
A jq invocation takes zero or more options, a single filter expression (a string), and one or more input files or stdin. Every filter is applied to each top-level JSON value in the input, and results are pretty-printed to stdout by default.
jq [OPTIONS] FILTER [FILE...]
cat data.json | jq FILTER
curl -s https://api.example.com | jq '.'
Output: (none — exits 0 on success)
Essential options
| Option | Meaning |
|---|---|
-r | Raw output (unquoted strings) |
-c | Compact output (no pretty print) |
-n | Null input (create JSON without stdin) |
-e | Exit 1 if last output is false/null |
-s | Slurp: read all inputs into an array |
-R | Raw input: treat input as raw strings |
-j | Join output (like -r but no trailing newline) |
--arg NAME VAL | Pass shell variable as string |
--argjson NAME VAL | Pass shell variable as JSON |
--slurpfile NAME FILE | Load file as JSON into variable |
--tab | Use tabs for indentation |
--indent N | Use N spaces |
--raw-output0 | NUL-separate raw outputs (jq 1.7+) — pairs with xargs -0 |
-L DIR / --library-path DIR | Add jq module search path (long form added in 1.8) |
JQ_COLORS env | Customise output colors; truecolor escapes accepted in 1.8 |
NO_COLOR env | Disable colored output (jq 1.7+, honours no-color.org) |
Basic filters
The identity filter . passes input through unchanged (useful for pretty-printing). Field access uses .key for objects and .[N] for arrays; .[] iterates over every element and outputs each as a separate value. Appending ? to any filter suppresses errors when the path does not exist.
jq '.' # pretty print (identity)
jq '.key' # get object field
jq '.key.nested' # nested field
jq '.key?' # optional (no error if missing)
jq '.[0]' # array index (0-based)
jq '.[-1]' # last element
jq '.[2:5]' # array slice (indices 2,3,4)
jq '.[]' # iterate over array/object values
jq 'keys' # array of object keys
jq 'values' # array of object values
jq 'length' # length of array/string/object
jq 'type' # "null","boolean","number","string","array","object"
Output: (none — exits 0 on success)
Given {"name":"Alice","age":30,"city":"NYC","scores":[95,87,92],"active":true}:
Output:
{
"name": "Alice",
"age": 30,
"city": "NYC",
"scores": [95, 87, 92],
"active": true
}
jq '.name' → "Alice" · jq '.[0]' on [95,87,92] → 95 · jq 'keys' → ["active","age","city","name","scores"] · jq 'length' on the scores array → 3 · jq 'type' → "object"
Comma and pipe
Comma (,) runs two filters against the same input and concatenates their outputs. The pipe (|) passes the output of the left filter as input to the right, enabling step-by-step transformations identical in spirit to Unix shell pipes.
jq '.name, .age' # output multiple fields (separate lines)
jq '.items | length' # pipe: get array length
jq '.items[] | .name' # iterate items, get name field
jq '.data | .[] | select(.active)' # chain
Output: (none — exits 0 on success)
Given {"name":"Alice","age":30} and an array [{"name":"Alice","dept":"Eng"},{"name":"Bob","dept":"Ops"}]:
Output:
"Alice"
30
jq '.[] | .name' on the array:
"Alice"
"Bob"
Object and array construction
Wrap a filter in {} to build a new JSON object, and in [] to collect multiple outputs into an array. Inside {}, shorthand {foo} expands to {foo: .foo}; wrap the key in parentheses to evaluate it as an expression.
jq '{name: .name, age: .age}' # build new object
jq '{(.key): .value}' # dynamic key from expression
jq '[.items[] | .id]' # build array from iteration
jq '[ .[] | select(.score > 50) ]' # array of filtered elements
jq '{"total": (.items | length)}' # computed field
Output: (none — exits 0 on success)
Given {"name":"Alice","age":30,"city":"NYC"}:
Output:
{
"name": "Alice",
"age": 30
}
select — filtering
select(condition) passes its input through unchanged when the condition is truthy, and produces no output otherwise. It is the primary way to filter elements out of an iteration — chain it after .[] to keep only matching items.
jq '.[] | select(.active == true)'
jq '.[] | select(.score > 80)'
jq '.[] | select(.name | startswith("A"))'
jq '.[] | select(.tags | contains(["linux"]))'
jq '.[] | select(.status == "error" or .status == "warn")'
jq '.[] | select(.value != null)'
jq '.[] | select(has("email"))'
jq '.[] | select(.count >= 10 and .count <= 100)'
Output: (none — exits 0 on success)
Given [{"name":"Alice","dept":"Eng","active":true},{"name":"Bob","dept":"Ops","active":false}]:
Output:
{
"name": "Alice",
"dept": "Eng",
"active": true
}
map and map_values
map(f) is shorthand for [.[] | f] — it applies a filter to every element of an array and collects the results. map_values(f) does the same for every value in an object, preserving the keys.
jq 'map(.price * 1.1)' # transform each element
jq 'map(select(.active))' # filter array
jq 'map({id, name})' # project fields (shorthand)
jq 'map(.tags[])' # flatten one level
jq '[.[] | .name] | map(ascii_upcase)' # uppercase all names
jq 'map_values(. + 1)' # increment every value in object
Output: (none — exits 0 on success)
Given [{"id":1,"name":"Alice","active":true},{"id":2,"name":"Bob","active":false}]:
Output:
[
{
"id": 1,
"name": "Alice"
}
]
jq '[.[] | .name] | map(ascii_upcase)':
[
"ALICE",
"BOB"
]
String operations
jq provides format strings (@base64, @uri, @csv, @tsv, @html, @json, @sh) that convert a value to a specific encoding — they are most useful with -r to strip the surrounding JSON quotes. String interpolation with "\(.expr)" embeds any filter result inline.
jq '.name | ascii_upcase'
jq '.name | ascii_downcase'
jq '.name | ltrimstr("prefix")'
jq '.name | rtrimstr(".json")'
jq '.name | split("/")' # split string → array
jq '.parts | join(", ")' # join array → string
jq '.s | test("^Error")' # regex test → boolean
jq '.s | match("([0-9]+)")' # regex match object
jq '.s | gsub("foo"; "bar")' # regex replace (jq 1.6+)
jq '"\(.name) is \(.age) years old"' # string interpolation
jq '@base64' # base64 encode
jq '@base64d' # base64 decode
jq '@uri' # URL-encode
jq '@html' # HTML-escape
jq '@csv' # format as CSV row
jq '@tsv' # format as TSV row
jq '@json' # serialize to JSON string
Output: (none — exits 0 on success)
Given {"name":"Alice","age":30}:
Output:
"ALICE"
"alice"
"Alice is 30 years old"
jq -r '"\(.name) is \(.age) years old"':
Alice is 30 years old
jq -r '[.name, (.age | tostring)] | @csv':
"Alice",30
jq -r '[.name, (.age | tostring)] | @tsv':
Alice 30
Math and comparisons
jq supports standard arithmetic (+, -, *, /, %) and comparison operators (==, !=, <, >, <=, >=). Aggregation builtins — sort_by, group_by, unique_by, min_by, max_by, add — work on arrays and are typically combined with map or .[] pipelines.
jq '.price | floor'
jq '.value | ceil'
jq '.x | sqrt'
jq '.a + .b'
jq 'if .score >= 90 then "A" elif .score >= 80 then "B" else "C" end'
jq '.items | sort_by(.name)'
jq '.items | sort_by(.price) | reverse'
jq '.items | unique_by(.category)'
jq '.items | group_by(.status)'
jq '.items | min_by(.price)'
jq '.items | max_by(.score)'
jq '[.[] | .price] | add' # sum all prices
jq '[.[] | .price] | add / length' # average price
Output: (none — exits 0 on success)
Given {"score":85}:
Output:
"B"
jq '[.[] | .price] | add' on [{"price":10},{"price":25},{"price":15}]:
50
jq '.items | sort_by(.name)' on an array of objects:
[
{"name": "Alice", "dept": "Eng"},
{"name": "Bob", "dept": "Ops"},
{"name": "Carol", "dept": "Eng"}
]
Recursive and flatten
.. is the recursive descent operator — it produces every value at every level of nesting, making it useful for searching deeply nested documents. flatten collapses nested arrays into a single flat array; pass a depth argument to limit how many levels are collapsed.
jq '.. | .name? // empty' # recurse all levels
jq '[.. | numbers]' # all numbers anywhere in tree
jq 'flatten' # flatten nested arrays completely
jq 'flatten(1)' # flatten one level
jq 'paths' # all paths in the document
jq 'paths(scalars)' # paths to scalar values
jq 'getpath(["a","b"])' # value at path
jq 'setpath(["a","b"]; 99)' # set value at path
jq 'delpaths([["a","b"]])' # delete at path
Output: (none — exits 0 on success)
Given [[1,[2,3]],[4,5]]:
Output:
[1, 2, 3, 4, 5]
jq 'flatten(1)':
[1, [2, 3], 4, 5]
reduce and foreach
reduce folds a stream into a single value by accumulating state across each input; foreach is similar but emits intermediate results at each step, making it useful for running totals or streaming transformations.
# Sum all numbers in array
jq 'reduce .[] as $x (0; . + $x)'
# Running totals
jq '[foreach .[] as $x (0; . + $x)]'
# Group and sum
jq 'group_by(.category) | map({key: .[0].category, value: map(.amount) | add}) | from_entries'
Output: (none — exits 0 on success)
Given [10, 20, 30, 40]:
Output:
100
jq '[foreach .[] as $x (0; . + $x)]':
[10, 30, 60, 100]
jq 'group_by(.category) | map({key: .[0].category, value: map(.amount) | add}) | from_entries' on [{"category":"food","amount":12},{"category":"travel","amount":80},{"category":"food","amount":8}]:
{
"food": 20,
"travel": 80
}
Input from shell variables
Pass shell values into a jq filter without string interpolation hacks. --arg NAME VALUE injects a shell string as a jq string variable $NAME; --argjson NAME JSON parses the value as JSON first, so numbers and booleans stay typed rather than becoming strings.
jq --arg name "Alice" '.[] | select(.name == $name)'
jq --argjson min 50 '.[] | select(.score > $min)'
jq -n --arg ts "$(date -Iseconds)" '{"timestamp": $ts}'
# Pass entire JSON from shell
DATA='{"key":"val"}'
echo "$DATA" | jq --argjson extra '{"extra":true}' '. + $extra'
Output:
{
"name": "Alice",
"dept": "Eng",
"active": true
}
jq -n --arg ts "2026-04-26T14:30:00+00:00" '{"timestamp": $ts}':
{
"timestamp": "2026-04-26T14:30:00+00:00"
}
echo "$DATA" | jq --argjson extra '{"extra":true}' '. + $extra':
{
"key": "val",
"extra": true
}
Null coalescing and defaults
The // operator returns the left side unless it is null or false, in which case it returns the right side. Use it to supply fallback values when fields may be missing, or chain it with empty to silently drop null outputs.
jq '.missing // "default"' # use "default" if null/false
jq '.x // empty' # produce no output if null/false
jq '.[] | .name // "unknown"' # fallback per element
Output: (none — exits 0 on success)
Given {"name":null,"city":"NYC"}:
Output:
"default"
"unknown"
"NYC"
Practical recipes
# Pretty-print and paginate
curl -s https://api.example.com/users | jq '.' | less
# Extract a flat list of values (one per line)
jq -r '.[].name' users.json
# CSV export
jq -r '.[] | [.id, .name, .email] | @csv' users.json
# Convert array of objects to key=value lines
jq -r '.[] | "\(.key)=\(.value)"' config.json
# Count items by field
jq 'group_by(.status) | map({key: .[0].status, value: length}) | from_entries' events.json
# Merge two JSON files
jq -s '.[0] * .[1]' base.json override.json
# Slurp multiple JSON lines (NDJSON) into array
jq -s '.' < ndjson.txt
# Filter NDJSON stream without slurping (memory-efficient)
jq -c 'select(.level == "error")' < ndjson.txt
# Compact all objects in array
jq -c '.[]' data.json
# Update a field in-place (print modified JSON)
jq '.version = "2.0"' package.json
# Delete a key
jq 'del(.password)' user.json
# Rename a key
jq '{new_name: .old_name} + del(.old_name)' obj.json
# Add element to array
jq '.tags += ["new-tag"]' item.json
# Get Docker container IPs
docker inspect $(docker ps -q) | jq '.[].NetworkSettings.Networks[].IPAddress' -r
# Parse GitHub API response
curl -s "https://api.github.com/repos/torvalds/linux/releases" \
| jq -r '.[0] | "Latest: \(.tag_name) — \(.published_at)"'
# AWS CLI output processing
aws ec2 describe-instances | jq -r \
'.Reservations[].Instances[] | "\(.InstanceId)\t\(.State.Name)\t\(.PublicIpAddress // "N/A")"'
Output: (none — exits 0 on success)
jq -r '.[].name' users.json on [{"id":1,"name":"Alice","email":"alice@example.com"},{"id":2,"name":"Bob","email":"bob@example.com"}]:
Output:
Alice
Bob
jq -r '.[] | [.id, .name, .email] | @csv':
1,"Alice","alice@example.com"
2,"Bob","bob@example.com"
jq 'group_by(.status) | map({key: .[0].status, value: length}) | from_entries' on an events array:
{
"error": 3,
"info": 12,
"warn": 5
}
jq -c 'select(.level == "error")' on NDJSON input:
{"ts":"2026-01-15T10:23:45Z","level":"error","msg":"connection refused"}
{"ts":"2026-01-15T10:45:01Z","level":"error","msg":"timeout"}
jq '.version = "2.0"' package.json:
{
"name": "my-app",
"version": "2.0",
"description": "Example app"
}
AWS CLI example output:
i-0a1b2c3d4e5f running 54.210.123.45
i-1f2e3d4c5b6a stopped N/A
Debugging
jq '. | debug' # print to stderr + pass through
jq '. | debug("msg: \(.key)")' # custom debug message (jq 1.6+)
jq 'error("bad input")' # raise an error
jq 'halt_error(1)' # exit with code
Output:
["DEBUG:","msg: Alice"]
{
"name": "Alice",
"age": 30
}
Use
jq -n(null input) to build JSON from scratch without needing an input file:jq -n '{name: "Alice", ts: now | todate}'. Thenowbuiltin returns the current Unix timestamp.
What's new in jq 1.7 / 1.8
After a five-year gap, jq 1.7.0 shipped in September 2023, followed by 1.7.1 (December 2023) and 1.8.0 (June 2024). Together they added long-requested builtins (pick, INDEX, IN, JOIN, trim, abs, toboolean, skip), fixed several CVEs, and made breaking changes to string indexing and assignment semantics. Check jq --version before relying on anything below.
Selecting fields with pick (1.7)
pick(path_expression) projects an object down to only the named paths and returns null for missing leaves. It is the long-missing inverse of del and the typed alternative to with_entries(select(...)).
jq 'pick(.a, .b.c, .x)' # {"a":1,"b":{"c":2},"x":null}
jq 'pick(.[] | .id)' # keep only .id on every array element
Output: given {"a":1,"b":{"c":2,"d":3},"e":4}:
{
"a": 1,
"b": {
"c": 2
},
"x": null
}
SQL-style INDEX, IN, JOIN (1.7)
These three builtins make join-style work over arrays of objects far more readable than hand-rolled reduce pipelines:
INDEX(stream; key_expr)— turn a stream of objects into a lookup table keyed bykey_expr.IN(stream)— boolean test, true when the input value appears anywhere instream.JOIN($idx; key_expr; join_expr)— like a SQLLEFT JOINagainst anINDEX-built table.
# Build a lookup table from an array
jq 'INDEX(.id)' users.json
# {"u1":{"id":"u1","name":"Alice"}, "u2":{...}}
# Filter to rows whose status is in an allowlist stream
jq '.[] | select(.status | IN("error","warn"))' events.json
# Join orders against a users index
jq -n --slurpfile orders orders.json --slurpfile users users.json '
$orders[0] | JOIN(($users[0] | INDEX(.id)); .user_id; .[0] + {user: .[1].name})
'
Output: (none — exits 0 on success)
Trim, abs, toboolean, skip (1.8)
trim/0, ltrim/0, rtrim/0 strip whitespace from strings (no need for gsub("^\\s+|\\s+$"; "") anymore). trimstr/1 strips a specific substring from both ends. abs returns absolute value while preserving the original numeric literal (unlike fabs, which forces a float). toboolean parses the strings "true"/"false" into booleans. skip($n; f) is the dual of limit($n; f).
jq '" hello " | trim' # "hello"
jq '"foo.bar.foo" | trimstr("foo")' # ".bar."
jq '-42 | abs' # 42
jq '"true" | toboolean' # true
jq '[skip(2; range(5))]' # [2,3,4]
Output: (none — exits 0 on success)
NUL-separated output for safe pipelines (1.7)
--raw-output0 emits raw strings separated by NUL bytes instead of newlines, so filenames containing newlines round-trip safely through xargs -0, cpio -0, or find -print0 pipelines.
jq -r --raw-output0 '.files[]' manifest.json | xargs -0 ls -l
Output: (none — exits 0 on success)
Color controls (1.7)
NO_COLOR=1 (the no-color.org convention) and the expanded JQ_COLORS env var let you theme jq output without flags. jq 1.8 accepts truecolor (2;R;G;B) escapes inside JQ_COLORS:
NO_COLOR=1 jq '.' data.json
JQ_COLORS="1;30:0;37:0;37:0;37:0;32:1;37:1;37:2;37" jq '.' data.json
Output: (none — exits 0 on success)
Breaking changes worth knowing
- String indexing is now in code points, not bytes.
indices,index,rindex,splits,[a:b]on strings count Unicode code points in 1.8 — scripts that hand-counted UTF-8 byte offsets may produce different results. tonumberrejects whitespace." 12 " | tonumbernow errors; pipe throughtrimfirst.- Removed:
leaf_paths,recurse_down,pow10, the private_nwisefilter. Usepaths(scalars),recurse,exp10instead. --indent 0no longer implies compact. Use-cfor compact output.- Array/object size cap of 2^29 (≈537M) elements (CVE-2024-23337 mitigation).
Modern alternatives
jq is the de-facto standard, but several actively-maintained tools cover adjacent use cases — faster execution, additional input formats, binary parsing, or a grep-friendly representation. Pick by data format and workflow rather than treating any of these as a drop-in replacement.
| Tool | Language | Strength | When to reach for it |
|---|---|---|---|
jaq | Rust | Faster startup and execution than jq 1.7 on most benchmarks; jq-language compatible | Many small JSON files, CI pipelines, perf-critical scripts |
gojq | Go | Pure-Go jq with YAML I/O and better error messages | Single static binary in container images; YAML pipelines |
yq (Mike Farah) | Go | YAML-first processor; reads/writes JSON, XML, CSV, TOML, properties | Editing Kubernetes manifests, Helm values, GitHub Actions workflows |
dasel | Go | Multi-format selector for JSON, YAML, TOML, XML, CSV with a single query language | Cross-format ETL where the input/output formats differ |
fq | Go | jq language applied to binary formats (mp4, pcap, ELF, PNG, …) | Inspecting binary files with jq filters |
gron / fastgron | Go / C++ | Flatten JSON to greppable path = value; lines and back | Discovering the path you need before writing the jq filter; grep-only environments |
jless | Rust | Curses-style interactive JSON pager with collapsible nodes | Exploring unfamiliar JSON before reaching for a query |
fx | Go | Interactive viewer plus full-script transformations | Exploratory work and one-off ad-hoc transforms |
A common pairing is curl ... | gron | grep <keyword> to find the exact path, then write the equivalent jq filter once. For YAML pipelines, yq -o=json '.' file.yaml | jq '...' lets you keep using familiar jq syntax against YAML sources.
Sources
- jq 1.8 Manual (current) — official reference for the latest release.
- jq 1.7 release notes —
pick,INDEX/IN/JOIN,--raw-output0,JQ_COLORS,NO_COLOR. - jq 1.7.1 release notes — CVE-2023-50246 and CVE-2023-50268 fixes.
- jq 1.8.0 release notes —
trim/ltrim/rtrim,abs,toboolean,skip, code-point string indexing, semver versioning. - jaq — Rust jq clone — performance benchmarks vs jq 1.8.1 and gojq.
- gojq — pure Go jq — YAML support and improved error messages.
- yq (Mike Farah) — multi-format YAML/JSON/XML/CSV processor.
- dasel — single query language across JSON/YAML/TOML/XML/CSV.
- fq — jq for binary formats.
- gron / fastgron — greppable flattened JSON.
- structured-text-tools — curated index of JSON/YAML/XML/CSV CLI tools.