JQ tutorial: filter, select and reshape JSON with examples

By KeLoc Updated Read time 11 minutes

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

jq
"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

jq
"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

jq
"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:

QueryWhat happens
.orders1[]?.idPrints 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 // [])[].idPrints 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

jq
jq
jq
(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

jq
"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

jq
jq
"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

jq
{
  "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

jq
"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

jq
[
  {
    "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

jq
[
  "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

jq
[
  "MUG-01",
  "PAN-02",
  "TEA-04"
]

Count, sum and average (length, add)

Two built-ins cover most summaries:

  • length counts the items in an array, or the characters in a string.
  • add adds 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

jq
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

jq
[
  {
    "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

jq
{
  "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:

macOS

Homebrew
brew install jq

Don’t have Homebrew? Install it from brew.sh first.

Linux

Debian and Ubuntu
sudo apt-get install jq
Fedora
sudo dnf install jq

Most other distributions also package jq under the name jq.

Windows

winget
winget install jqlang.jq
Chocolatey
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:

  1. Download the file for your system from the official jq download page.
  2. Rename it to jq (jq.exe on Windows).
  3. On macOS and Linux, make it executable: chmod +x jq.
  4. 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:

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:

Terminal
# 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:

Terminal
jq -c '.orders[] | {id, total}' orders.json
Output
{"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:

Terminal
jq -r '.orders[].id' orders.json
Output
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:

Terminal
jq -r --arg status pending '.orders[] | select(.status == $status) | .id' orders.json
Output
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.

TaskFilterExample
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 itemsselect(test).orders[] | select(.total > 30)
Combine testsand, or, notselect(.status == "shipped" and .total > 50)
Build an object{key: value}{id, customer: .customer.name}
Transform each itemmap(f).orders | map(.total)
Sort by a fieldsort_by(.key).orders | sort_by(.total)
Remove duplicatesunique[.orders[].items[].sku] | unique
Count itemslength.orders | length
Add up numbersadd[.orders[].total] | add
Group by a fieldgroup_by(.key).orders | group_by(.status)
Default for nulla // b.coupon // "none"
Check a key existshas("key")select(has("coupon"))
Skip errors?.customer.name?
Loop over an array that may be missing.[]?.orders1[]?.id
Stop when a value is missinga // error("…").orders1 // error("missing orders1")
Print plain text-rjq -r '.orders[].id' orders.json
One line per result-cjq -c '.orders[]' orders.json
Pass a shell value--arg name valuejq --arg status pending '…' orders.json

Try your own JSON

DEV 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.