Most jq cheat sheets list syntax you'll never touch. This one runs on a single realistic document — an order with a customer, three line items, and shipping — and shows what each filter actually returns. Every output below was produced by jq 1.8.2 on this exact data, and you can reproduce any of them in the playground two clicks from now.
{
"order": {
"id": 4172,
"customer": { "name": "Dana Reyes", "tier": "gold",
"emails": ["[email protected]", "[email protected]"] },
"items": [
{ "sku": "KB-01", "name": "Mechanical Keyboard", "qty": 1, "price": 129 },
{ "sku": "DSK-04", "name": "Standing Desk", "qty": 1, "price": 549 },
{ "sku": "MAT-02", "name": "Desk Mat", "qty": 2, "price": 25 }
],
"shipping": { "method": "express", "cost": 18.50 },
"coupons": ["WELCOME10"]
}
}
Everything starts with . — the identity filter, the input itself. From there you navigate by field and index:
| Filter | Returns |
|---|---|
. | the whole document |
.order.customer.name | "Dana Reyes" |
.order.items[0].sku | "KB-01" |
.order.coupons[0] | "WELCOME10" |
.order.customer.emails | length | 2 |
.order.items | length | 3 |
.order | keys | ["coupons","customer","id","items","shipping"] |
.order.items[0] | keys | ["name","price","qty","sku"] |
.order.items | type | "array" |
.order | has("shipping") | true |
Missing keys return null rather than erroring — .order.customer.phone is null. Convenient for exploration, treacherous in scripts; more on that under errors.
.[] explodes a structure into a stream: each array element (or object value) becomes its own output. The pipe connects filters so each stage works on the last stage's output:
| Filter | Returns |
|---|---|
.order.items[] | .sku | "KB-01", "DSK-04", "MAT-02" (three outputs) |
[.order.items[].price] | [129, 549, 25] — brackets re-collect |
.order.items[] | .qty * .price | 129, 549, 50 |
.order.items | map(.qty * .price) | [129, 549, 50] — map is [ .[] | f ] |
.order.customer.emails | join("; ") | "[email protected]; [email protected]" |
That last table row is the mental unlock: map(f) is nothing but iterate-and-collect. Once you see jq as shell plumbing for JSON, unfamiliar filters decompose into familiar ones.
select() passes values that satisfy a condition and drops the rest — the WHERE clause of jq:
| Filter | Returns |
|---|---|
.order.items[] | select(.price > 100) | .name | "Mechanical Keyboard", "Standing Desk" |
.order.items | map(select(.name | contains("Desk"))) | length | 2 |
.order.items | sort_by(.price) | .[0].name | "Desk Mat" (cheapest) |
.order.items | sort_by(-(.qty * .price)) | .[0].name | "Standing Desk" (biggest line) |
.order.items | reverse | .[0].sku | "MAT-02" |
[.order.items[].price] | unique | length | 3 (no duplicate prices) |
.order.coupon // "none" | "none" — alternative operator |
Two idioms to internalize: negated sort keys (sort_by(-.price)) for descending order, and // for defaults when a field is null or false.
| Filter | Returns |
|---|---|
.order.items | map(.qty * .price) | add | 728 (subtotal) |
.order as $o | (… | add) + $o.shipping.cost | 746.5 (total with shipping) |
[.order.items[].price] | min | 25 |
[.order.items[].price] | max | 549 |
[.order.items[].price] | add / length | 234.333… (mean item price) |
.order.items | map(.qty) | add | 4 (total units) |
The as $var binding is how you compute with two distant parts of one document — bind the root, navigate away, come back. And 728 divided by 4 units is 182 even, the average price per unit:
.order as $o
| ($o.items | map(.qty * .price) | add) / ($o.items | map(.qty) | add)
# => 182
Half of jq's value is producing the shape the next tool wants, not just reading what's there:
| Filter | Returns |
|---|---|
{name: .order.customer.name, items: (.order.items|length)} | {"name":"Dana Reyes","items":3} |
.order.items[0] | to_entries | map(.key) | ["sku","name","qty","price"] |
.order.customer.name | ascii_upcase | "DANA REYES" |
.order.customer.name | @base64 | "RGFuYSBSZXllcw==" |
Object construction {…} builds exactly the fields you name; to_entries turns an object into key/value pairs you can map over; @base64 and friends encode for transport.
String interpolation — \(expression) inside a string — is how jq becomes a report writer:
.order.items[] | "\(.sku): \(.qty) x $\(.price)"
# with -r (raw output):
# KB-01: 1 x $129
# DSK-04: 1 x $549
# MAT-02: 2 x $25
"order #\(.order.id) — \(.order.items | length) items"
# => order #4172 — 3 items
Under -r, string results print unquoted, which is exactly what you want for lines headed to a file or a shell loop.
.order.customer.phone → null, silently. Catch it with // "default" or test with has("phone")..order.id | .foo errors ("Cannot index number with foo"). Append ? — .foo? — to suppress and get null instead.lengthh fails before any input runs; jq reports jq: 1 compile error with a caret at the offending token.. stages — run .order.items, then .order.items[], then the full pipeline — until the surprise appears.The playground runs real jq 1.8.2 via WebAssembly — paste a document, type a filter, watch output and errors live.
Open the jq Playground →Navigate with .a.b[0], stream with .[], chain with |, filter with select(), transform each with map(), collect with […], bind with as $x, default with //, report with "\(...)" and -r. That's nine ideas and roughly 95% of daily jq. When your data isn't JSON yet, the JSON formatter will tell you why, the JSON viewer shows structure at a glance, and the JSON to YAML converter bridges the config world.
jq is a filter language for JSON: you give it a document and an expression like .items[] | select(.price > 100) | .name, and it streams out the values that match. It runs in the terminal, so it plugs into pipes — curl something, jq it, done.
It feeds one filter's output into the next, exactly like a shell pipe. .order.items | length first navigates to the array, then counts it. Every nontrivial jq program is a chain of small filters connected by pipes, which is why jq one-liners read left to right as a sentence.
Plain . is the input unchanged; .[] iterates. On an array it emits each element as a separate output; on an object it emits each value. So [.order.items[] | .price] wraps iteration in brackets to collect results back into an array, while .order.items[] | .price leaves them as a stream.
By design: field access on a missing key is null, not a failure, so filters survive ragged data. That convenience hides bugs — null flows through the pipeline silently. Use .foo? to suppress errors on wrong types, // "default" to substitute for null, and strict error() in scripts where you'd rather crash.
Pass -r. Without it, jq's output is valid JSON, so strings print quoted; with -r, string results print as their raw contents. It's the difference between "[email protected]" and [email protected], and it's what makes jq usable for feeding values into shell scripts.