Practical guide

How to use JQ: practical JSON filter examples

JQ is a query language for JSON, but querying is only half the job: it can reshape data too. From one source document, an expression might pull a field, trim an object, filter a list, or build a grouped summary.

Run these JQ expressions against the sample data below, then adapt them to your own JSON. JSON All-in-One provides a local workspace for trying the examples with an opened file or browser response.

Use one small orders fixture

The examples below use an array of orders with nested customer data, optional coupons, nullable regions, and repeated line items. Keeping one fixture makes it easier to see what each expression changes.

In the CLI, save the fixture as orders.json and run jq 'FILTER' orders.json, replacing FILTER with one of the examples below. In JSON All-in-One, open the JSON source, focus the JQ input, paste the expression, and run it locally against the root value.

orders.jsonjson
[
  {"id":"ord_1001","status":"paid","region":"EU","customer":{"name":"Lin","plan":"pro"},"total":129.5,"items":[{"sku":"api","qty":2},{"sku":"seat","qty":3}]},
  {"id":"ord_1002","status":"trial","region":null,"customer":{"name":"Mara","plan":"free"},"total":0,"items":[{"sku":"seat","qty":1}]},
  {"id":"ord_1003","status":"paid","region":"US","customer":{"name":"Noor","plan":"pro"},"total":349,"items":[{"sku":"api","qty":10}],"coupon":"LAUNCH"}
]

Extract nested fields

Use .[] to iterate the root array. After that, paths start from each order, so .customer.name means the customer name on the current order, not on the root document.

If you want one object per order, wrap the fields in {}. JQ lets you rename fields at the same time, which is useful before exporting a smaller result.

one row per orderjq
.[] | {order: .id, name: .customer.name, plan: .customer.plan, total}

Filter records with select

select(...) keeps or drops the current input. It does not build a new object by itself. In this example, .[] emits each order, select(.status == "paid") keeps only paid orders, and the final object controls the output shape.

Use map(...) when you want the final result to stay as an array. That difference is often the source of confusing output when a later export expects one array value.

streamed paid ordersjq
.[] | select(.status == "paid") | {id, total, region}
paid orders as an arrayjq
map(select(.status == "paid") | {id, total, region})

Handle missing and null

A missing field and a present null are not the same thing. The // operator supplies a fallback when the left side is null or false, and has("coupon") tells you whether the field exists.

Be deliberate here. If region: null means unknown and a missing coupon means no coupon, preserve that difference unless the receiving tool truly needs one simplified value.

review optional fieldsjq
map({
  id,
  region: (.region // "unassigned"),
  hasCoupon: has("coupon"),
  coupon: (.coupon // null)
})

Group and summarize

group_by expects compatible values and is normally used after sorting by the same key. The expression below sorts by status, groups orders with the same status, and creates a summary object for each group.

This is a whole-input operation: JQ needs the records available to sort and group them. On very large data, test this separately from simply opening the file.

totals by statusjq
sort_by(.status)
| group_by(.status)
| map({
    status: .[0].status,
    orders: length,
    revenue: (map(.total) | add)
  })

Follow one realistic cost-allocation workflow

A longer workflow is more useful than two isolated screenshots. The video below starts with an August cloud statement containing 1,200 cost lines, narrows it to 623 production charges, groups those charges by team, adds each total, and sorts the five resulting teams from highest spend to lowest.

The finished expression remains visible while the result is checked against known fixture totals. The source stays open in its own tab, so the last step is not another transformation: it is a review of currency, production tags, credits, and the reporting boundary with the finance owner.

production spend by teamjq
.lineItems
| map(select(.environment == "production"))
| group_by(.team)
| map({
    team: .[0].team,
    total_usd: (((map(.costUsd) | add) * 100 | round) / 100)
  })
| sort_by(-.total_usd)
Complete cloud-cost workflow: search a 1,200-line statement, build a readable jq filter through autocomplete, reduce 623 production charges to five team totals, verify the leading values, and return to the untouched source for business checks.Watch on YouTube

Small task reference

When JQ feels difficult, translate the task into a pipeline: choose the input, optionally filter it, then shape the output. Avoid mixing JSONPath syntax such as $[0] or @.field into JQ; JQ has its own path and iterator syntax.

For selection-only questions, JSONPath examples may be simpler. For transformation and export preparation, JQ is usually the better fit.

TaskExpression
First order id.[0].id
All customer names.[].customer.name
Paid orders onlymap(select(.status == "paid"))
Rows for exportmap({id, customer: .customer.name, total})