JQ tutorial: filter, select and reshape JSON with examples
JSON is being used everywhere in the software industry. There comes a time you need to take a look json data to find/extract few things. You might need to write script in other editor, and run it from editor or terminal. Lot of move around to just to peek at the data. I have been there. you might probably there too. That's where JQ will come in handy.
JQ is a small command-line tool for reading and reshaping JSON data. You give it a filter, such as .orders[].id. To filter a JSON array, loop over it and keep the items you want with select(): .orders[] | select(.status == "pending").
You can run the examples in this page and try it out in the interactive session.
Find customers with orders over 30
"Ana Silva"
"Chen Wei"
Change 30 to 10 and run it. The sample orders are below.
How jq works
JQ reads JSON, passes it through a filter, and prints the result as JSON. Filters connect with a pipe (|): the output of the left side becomes the input of the right side, the same way pipes work in a shell.
The smallest filter is a single dot (.). It passes the input through unchanged, which is why jq . is the quickest way to pretty-print a minified API response.
The sample: three orders
Every example runs against this sample json response. Only the first order has a coupon, and the last customer’s email is null. APIs do this all the time, and the section on missing keys shows how to deal with it.
Sample orders JSON
{
"store": "north",
"currency": "USD",
"orders": [
{
"id": "A-1001",
"status": "shipped",
"customer": { "name": "Ana Silva", "email": "ana@example.com" },
"items": [
{ "sku": "MUG-01", "qty": 2, "price": 12.5 },
{ "sku": "TEA-04", "qty": 1, "price": 8 }
],
"total": 33,
"coupon": "WELCOME10"
},
{
"id": "A-1002",
"status": "pending",
"customer": { "name": "Ben Okafor", "email": "ben@example.com" },
"items": [
{ "sku": "MUG-01", "qty": 1, "price": 12.5 }
],
"total": 12.5
},
{
"id": "A-1003",
"status": "shipped",
"customer": { "name": "Chen Wei", "email": null },
"items": [
{ "sku": "PAN-02", "qty": 1, "price": 64 },
{ "sku": "TEA-04", "qty": 3, "price": 8 }
],
"total": 88
}
]
}
Get a field (.key)
Write a dot followed by the key name. Chain keys to go deeper, and use [0] to pick an array item by position. Missing order in the path will not throw an error. It simply returns null.
Get the first customer’s name
"Ana Silva"
If a key contains a dash or a space, quote it: ."order-id". Without the quotes, jq reads the dash as subtraction.
Loop over an array ([])
Empty brackets after an array send each item through the rest of the filter, one at a time. Read .orders[].id as “for every order, give me its id”.
List every order id
"A-1001"
"A-1002"
"A-1003"
The result is three separate values, not an array. Wrap the whole filter in brackets to collect them: [.orders[].id]. Forgetting this is the usual reason a later length or sort doesn’t do what you expect.
When the array is missing (“Cannot iterate over null”)
When a key doesn't exist, jq gives null, like we saw it in the get key, and [] it can’t loop over null. The sample json has no orders1 key, so .orders1[].id stops with jq: error: Cannot iterate over null (null). Below methods provide predictable ways to query instead:
| Query | What happens |
|---|---|
.orders1[]?.id | Prints nothing: ? hides the error. Similar to try..catch in other langauges. Fine when the array is optional, but ? also hides other mistakes, such as orders1 being a string. |
(.orders1 // [])[].id | Prints nothing: // provides alternative to the missing value (order1 is missing). Only a missing or null array counts as empty; a string still reports an error. |
.orders1 // error("missing orders1") | Stops with jq: error: missing orders1. Use it when a script must not carry on without the data. |
Try the three ways on the missing orders1 key
(no output)
Don’t write .orders1 | error("missing orders1"). error stops with whatever it receives, so that query fails even when the array exists. After // it runs only when the left side is null or false.
Take part of an array ([start:end])
Slices work like Python: [0:2] takes items 0 and 1, and the end index is not included. Leave out a side to run to the start or end.
Take the first two orders
"A-1001"
"A-1002"
A negative index counts from the end: .orders[-1] is the last order, and [-2:] takes the last two.
Take orders from the end of the list
"A-1003"
Keep matching items (select)
select(condition) passes its input through when the condition is true and drops it otherwise.
Keep only pending orders
{
"id": "A-1002",
"status": "pending",
"customer": {
"name": "Ben Okafor",
"email": "ben@example.com"
},
"items": [
{
"sku": "MUG-01",
"qty": 1,
"price": 12.5
}
],
"total": 12.5
}
Writing .orders | select(…) will apply filter on orders as single item. If you want the filter to be applied on items in the array, orders[] should be used, so you get every order or none.
Filtering a large export? Dev Workbench runs the same jq queries on local files on your Mac, and Pro opens files up to 1 GB.
Combine conditions (and, or, not)
Join tests with and and or, and add parentheses when you mix them. This finds shipped orders worth more than 50:
Find shipped orders over 50
"A-1003"
not is a filter rather than an operator, so it goes after a pipe: select(.coupon | not) keeps the orders without a coupon.
Build a smaller object ({…})
Curly braces build a new object. Use key: filter to rename a field or reach into nested data. When the name stays the same, the key alone is enough: {id, total} is short for {id: .id, total: .total}.
Build a short summary of each order
[
{
"id": "A-1001",
"customer": "Ana Silva",
"total": 33
},
{
"id": "A-1002",
"customer": "Ben Okafor",
"total": 12.5
},
{
"id": "A-1003",
"customer": "Chen Wei",
"total": 88
}
]
Transform and sort arrays (map, sort_by, unique)
map(f) runs a filter on every item and keeps the results in an array; it is the same as [.[] | f]. sort_by(.key) orders an array of objects from smallest to largest. Add | reverse when you want the largest first.
Sort order ids by total, smallest first
[
"A-1002",
"A-1001",
"A-1003"
]
unique sorts an array and removes duplicates, which makes it a quick way to list every distinct value of a field:
List every SKU sold, without duplicates
[
"MUG-01",
"PAN-02",
"TEA-04"
]
Count, sum and average (length, add)
Two built-ins cover most summaries:
lengthcounts the items in an array, or the characters in a string.addadds up an array of numbers. Given strings it joins them, and given arrays it concatenates them.- Together they give an average:
add / length.
Average the order totals
44.5
(33 + 12.5 + 88) / 3 = 44.5.
Group and count (group_by)
group_by(.key) sorts an array and splits it into smaller arrays that share the same value. The raw result is nested and hard to read, so follow it with map to summarize each group:
Count orders per status
[
{
"status": "pending",
"count": 1
},
{
"status": "shipped",
"count": 2
}
]
Handle missing keys and nulls (//, has, ?)
Asking for a key that isn’t there is not an error in jq. You get null, which is why a typo in a field name prints null instead of failing. The alternative operator // supplies a default:
Show each coupon, or “none” when it is missing
{
"id": "A-1001",
"coupon": "WELCOME10"
}
{
"id": "A-1002",
"coupon": "none"
}
{
"id": "A-1003",
"coupon": "none"
}
// uses the right side whenever the left side is null or false, so .customer.email // "no email" also covers the customer whose email is null. When you need to know whether a key exists at all, test it with has("coupon").
If an API sometimes sends a string where you expect an object, .customer.name stops with “Cannot index string”. Adding ?, as in .customer.name?, skips those values instead. For an array that may be missing, see When the array is missing.
Install jq
jq is a single program with no other dependencies. Run jq --version first; if it prints a version such as jq-1.7.1, you already have it. Otherwise install it with your system’s package manager:
Linux
sudo apt-get install jq
sudo dnf install jq
Most other distributions also package jq under the name jq.
Windows
winget install jqlang.jq
choco install jq
PowerShell accepts the single-quoted filters used on this page. In Command Prompt, wrap the filter in double quotes instead, for example jq ".orders[].id" orders.json.
No package manager? Use a standalone executable instead:
- Download the file for your system from the official jq download page.
- Rename it to
jq(jq.exeon Windows). - On macOS and Linux, make it executable:
chmod +x jq. - Move it to a folder on your PATH.
Open a new terminal window afterwards and run jq --version again to confirm.
Query an API response with curl
Pipe the response straight into jq. Add -s (silent) to curl so its progress meter doesn’t clutter your terminal:
# Pretty-print the whole response
curl -s https://api.example.com/orders | jq .
# Pull one field from every order
curl -s https://api.example.com/orders | jq '.orders[].id'
If jq stops with a “parse error”, the response probably isn’t JSON, often an HTML error page. Run the same request with curl -i to see the status code and content type.
Query a JSON file
Put the filter in single quotes, then the file name. jq never changes the file; it prints the result, so redirect the output when you want to keep it:
# Print the pending orders
jq '.orders[] | select(.status == "pending")' orders.json
# Save a smaller summary to a new file
jq '[.orders[] | {id, total}]' orders.json > summary.json
Add -c for one compact line per result, which is easier to scan, grep or feed into another tool:
jq -c '.orders[] | {id, total}' orders.json
{"id":"A-1001","total":33}
{"id":"A-1002","total":12.5}
{"id":"A-1003","total":88}
Use jq in shell scripts (-r, --arg)
By default jq prints strings with their quotes, because the output is still JSON. Add -r (raw output) when you want plain text for a shell script or another tool:
jq -r '.orders[].id' orders.json
A-1001
A-1002
A-1003
To use a shell value inside a filter, pass it with --arg instead of pasting it into the quotes. jq receives it as a string named $status:
jq -r --arg status pending '.orders[] | select(.status == $status) | .id' orders.json
A-1002
--arg always passes a string. Use --argjson for numbers and booleans, for example --argjson min 50.
jq cheat sheet
The filters from this page in one place. Examples use the sample orders.
| Task | Filter | Example |
|---|---|---|
| Pretty-print JSON | . | jq . orders.json |
| Get a field | .key | .currency |
| Get a nested field | .a.b | .orders[0].customer.name |
| Loop over an array | .[] | .orders[].id |
| Collect results into an array | [ … ] | [.orders[].id] |
| Slice an array | .[start:end] | .orders[:2] |
| Last item | .[-1] | .orders[-1] |
| Keep matching items | select(test) | .orders[] | select(.total > 30) |
| Combine tests | and, or, not | select(.status == "shipped" and .total > 50) |
| Build an object | {key: value} | {id, customer: .customer.name} |
| Transform each item | map(f) | .orders | map(.total) |
| Sort by a field | sort_by(.key) | .orders | sort_by(.total) |
| Remove duplicates | unique | [.orders[].items[].sku] | unique |
| Count items | length | .orders | length |
| Add up numbers | add | [.orders[].total] | add |
| Group by a field | group_by(.key) | .orders | group_by(.status) |
| Default for null | a // b | .coupon // "none" |
| Check a key exists | has("key") | select(has("coupon")) |
| Skip errors | ? | .customer.name? |
| Loop over an array that may be missing | .[]? | .orders1[]?.id |
| Stop when a value is missing | a // error("…") | .orders1 // error("missing orders1") |
| Print plain text | -r | jq -r '.orders[].id' orders.json |
| One line per result | -c | jq -c '.orders[]' orders.json |
| Pass a shell value | --arg name value | jq --arg status pending '…' orders.json |
Try your own JSON
Paste your own JSON or pick a sample, then run any query in the jq playground. It uses the whole window and, like the examples above, runs in your browser.
Open the jq playgroundDEV WORKBENCH FOR MAC
Run the same jq queries on local files.
Dev Workbench is a native Mac app with a JSON workbench built around jq. Open or paste JSON, get field and function suggestions as you type a query, and copy or save the result. Your files stay on your Mac.
- Field and function suggestions. Suggestions for fields and jq functions appear as you type a query.
- Pro: files up to 1 GB. Open large exports and logs with whole-file search and read-only jq queries.
Free download. Requires macOS 14 or later.