The jq Cheat Sheet: 25 Filters You'll Actually Use

⏱️ 9 min read🔧 Pairs with the jq Playground

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.

Advertisement

The example document

{
  "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"]
  }
}

Navigation: getting to the data

Everything starts with . — the identity filter, the input itself. From there you navigate by field and index:

FilterReturns
.the whole document
.order.customer.name"Dana Reyes"
.order.items[0].sku"KB-01"
.order.coupons[0]"WELCOME10"
.order.customer.emails | length2
.order.items | length3
.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.

Iteration and the pipe

.[] 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:

FilterReturns
.order.items[] | .sku"KB-01", "DSK-04", "MAT-02" (three outputs)
[.order.items[].price][129, 549, 25] — brackets re-collect
.order.items[] | .qty * .price129, 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.

Filtering and searching

select() passes values that satisfy a condition and drops the rest — the WHERE clause of jq:

FilterReturns
.order.items[] | select(.price > 100) | .name"Mechanical Keyboard", "Standing Desk"
.order.items | map(select(.name | contains("Desk"))) | length2
.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 | length3 (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.

Arithmetic and statistics

FilterReturns
.order.items | map(.qty * .price) | add728 (subtotal)
.order as $o | (… | add) + $o.shipping.cost746.5 (total with shipping)
[.order.items[].price] | min25
[.order.items[].price] | max549
[.order.items[].price] | add / length234.333… (mean item price)
.order.items | map(.qty) | add4 (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

Reshaping output

Half of jq's value is producing the shape the next tool wants, not just reading what's there:

FilterReturns
{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.

Strings that report for duty

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.

Errors and missing data

Try every filter on your own JSON

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 →

The 60-second recap

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.

Advertisement

Frequently Asked Questions

What is jq in one sentence?

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.

What does the pipe (|) do in jq?

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.

How is .[] different from plain .?

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.

Why does jq print null instead of erroring?

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.

How do I get raw strings without quotes?

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.

Related Tools