← All articles

jq Tutorial: Practical Examples, Filters, and a Copy-Paste Cheat Sheet

Learn jq with copy-paste commands for pretty-printing JSON, extracting fields, filtering arrays, reshaping API responses, passing shell variables, validating files, and exporting CSV.

jq is the command-line tool I reach for when a JSON response is too large to read, too nested for grep, or too sensitive to paste into a random web page. It parses JSON properly, applies a filter, and writes the result to standard output.

The shortest useful commands are:

jq . data.json                 # pretty-print and validate
jq -c . data.json              # compact JSON
jq -r '.data.token' resp.json  # raw string output, no quotes
jq '.items[] | .id' resp.json  # one id per array item
jq empty data.json             # validate only, no output

This tutorial uses realistic API-shaped examples instead of isolated tricks. By the end, you should be able to inspect a response, pull the fields you need, filter rows, reshape objects, pass shell variables safely, validate files in CI, and avoid the common jq errors that make beginners think the tool is stranger than it really is.

What jq Does

jq reads JSON from a file or standard input, runs a filter, and prints JSON again unless you ask for raw text.

Think of a jq filter as a tiny pipeline:

input JSON -> jq filter -> output JSON or text

The identity filter, ., means "return the current value unchanged." That is why jq . file.json is both a formatter and a syntax validator.

jq is best when the input is already valid JSON. If the payload has trailing commas, single quotes, comments, or half-copied text, run it through JSON Validator or JSON Fix first. jq is excellent at querying valid JSON; it is not a repair tool.

Install jq

# macOS
brew install jq

# Debian / Ubuntu
sudo apt-get install jq

# Windows
winget install jqlang.jq

# Verify the binary that your shell will run
jq --version

Most examples in this guide use single quotes around the jq filter. That is the least surprising option in macOS, Linux, zsh, bash, and PowerShell:

jq '.orders[] | .id' orders.json

If you are using Windows cmd.exe, quote handling is different. Use double quotes around the filter and escape inner double quotes, or run the command from PowerShell, Git Bash, WSL, or another shell with normal single-quote behavior.

Use One Sample File

Save this as orders.json if you want to run the examples locally:

{
  "orders": [
    {
      "id": "ord_1001",
      "status": "paid",
      "total": 42.5,
      "customer": { "id": 7, "name": "Ada", "tier": "pro" },
      "items": [
        { "sku": "json-pro", "qty": 1, "price": 29.5 },
        { "sku": "support", "qty": 1, "price": 13 }
      ],
      "tags": ["api", "renewal"]
    },
    {
      "id": "ord_1002",
      "status": "failed",
      "total": 0,
      "customer": { "id": 8, "name": "Lin", "tier": "free" },
      "items": [],
      "tags": ["trial"]
    },
    {
      "id": "ord_1003",
      "status": "paid",
      "total": 118,
      "customer": { "id": 9, "name": "Grace", "tier": "enterprise" },
      "items": [
        { "sku": "json-pro", "qty": 4, "price": 29.5 }
      ],
      "tags": ["api", "bulk"]
    }
  ],
  "nextCursor": null
}

Using the same file makes the filters easier to read. You can see when the current value is the whole document, one order, one item, or one string.

Pretty-Print, Minify, and Sort Keys

The identity filter returns the input:

jq . orders.json

That pretty-prints the file and validates it at the same time. If the JSON is malformed, jq prints a parse error and exits non-zero.

For compact output:

jq -c . orders.json

For stable diffs, sort object keys:

jq -S . orders.json

-S is useful before committing generated JSON, comparing API fixtures, or reviewing a config file where key order changes create noisy diffs. It does not sort arrays. If array order matters, sort those explicitly with sort_by.

Read Object Fields and Array Items

Use .key for object fields:

jq '.nextCursor' orders.json
# null

Use nested paths for nested objects:

jq '.orders[0].customer.name' orders.json
# "Ada"

Use .[] to iterate an array:

jq '.orders[]' orders.json

That emits three separate JSON values, one for each order. Once you are inside each order, select a field from the current value:

jq '.orders[] | .id' orders.json
# "ord_1001"
# "ord_1002"
# "ord_1003"

When you want plain strings for a shell script, add -r:

jq -r '.orders[] | .id' orders.json
# ord_1001
# ord_1002
# ord_1003

That difference matters. JSON strings include quotes; raw output does not.

Handle Missing Fields Without Breaking the Pipeline

If a field may be missing, use ? and //.

jq -r '.orders[] | .coupon? // "no coupon"' orders.json

The ? keeps jq from failing when the field does not exist. The // operator provides a fallback when the value is null or missing.

For optional arrays, default to an empty array before iterating:

jq '.discounts // [] | .[]' order.json

This pattern avoids the common Cannot iterate over null error.

Filter Arrays with select

Use select() when you want to keep only values that match a condition.

Paid orders:

jq '.orders[] | select(.status == "paid")' orders.json

Paid orders over 100:

jq '.orders[] | select(.status == "paid" and .total > 100)' orders.json

Orders tagged api:

jq '.orders[] | select(.tags | index("api"))' orders.json

Customers whose tier contains pro, case-insensitive:

jq '.orders[] | select(.customer.tier | test("pro"; "i"))' orders.json

For boolean fields, you do not need == true:

jq '.users[] | select(.active)' users.json

Use the explicit form only when it makes the intent clearer to the team reading the script later.

Build a New JSON Shape

Object construction is where jq becomes more than a viewer. You can turn a noisy API response into the small shape your script needs:

jq '.orders[] | {
  orderId: .id,
  customer: .customer.name,
  tier: .customer.tier,
  amount: .total
}' orders.json

That emits one object per order. To keep the result as a JSON array, wrap the iteration in brackets:

jq '[.orders[] | {
  orderId: .id,
  customer: .customer.name,
  tier: .customer.tier,
  amount: .total
}]' orders.json

If you already have an array as the current value, map() is often clearer:

jq '.orders | map({
  orderId: .id,
  customer: .customer.name,
  amount: .total
})' orders.json

Use the bracket form when you are streaming values from several places. Use map() when you are transforming one array into another array.

Count, Sum, Sort, and Group

length counts arrays, object keys, and string characters:

jq '.orders | length' orders.json
# 3

Sum order totals:

jq '[.orders[].total] | add // 0' orders.json
# 160.5

The // 0 fallback matters because add returns null for an empty array.

Sort orders by total:

jq '.orders | sort_by(.total)' orders.json

Sort descending:

jq '.orders | sort_by(.total) | reverse' orders.json

Group by status:

jq '.orders
  | sort_by(.status)
  | group_by(.status)
  | map({ status: .[0].status, count: length })' orders.json

group_by() expects sorted input for predictable results, so sort on the same key first.

Export JSON to CSV or TSV

jq can emit CSV rows with @csv:

jq -r '.orders[]
  | [.id, .customer.name, .status, .total]
  | @csv' orders.json

Output:

"ord_1001","Ada","paid",42.5
"ord_1002","Lin","failed",0
"ord_1003","Grace","paid",118

Add your own header row:

jq -r '(["id","customer","status","total"] | @csv),
  (.orders[] | [.id, .customer.name, .status, .total] | @csv)' orders.json

For logs and shell pipelines, TSV is often easier to read:

jq -r '.orders[] | [.id, .status, .total] | @tsv' orders.json

If you need nested flattening, stable headers, or spreadsheet-friendly type handling, use the full JSON to CSV guide. jq is great for quick exports, but production CSV conversion usually needs clearer rules.

Pass Shell Variables Safely

Do not splice shell variables into the jq filter string. Pass them with --arg for strings and --argjson for JSON values.

status="paid"
jq --arg status "$status" '.orders[] | select(.status == $status)' orders.json

For a numeric threshold:

min_total=50
jq --argjson min "$min_total" '.orders[] | select(.total >= $min)' orders.json

For a JSON array:

allowed='["pro","enterprise"]'
jq --argjson tiers "$allowed" \
  '.orders[] | select(.customer.tier as $tier | $tiers | index($tier))' \
  orders.json

--arg always creates a string. --argjson parses the value as JSON. Use --argjson only when you control the value or have already validated it.

Update and Redact JSON

jq writes to standard output. It does not edit files in place.

Set a field:

jq '.settings.debug = false' config.json

Update a field from its current value:

jq '.count |= . + 1' counter.json

Delete sensitive fields before sharing a sample:

jq 'del(.password, .secret, .token)' record.json

Redact nested keys in every order:

jq '.orders |= map(del(.customer.id))' orders.json

To save a change back to the same file, write to a temporary file and move it only if jq succeeds:

tmp=$(mktemp)
jq '.settings.debug = false' config.json > "$tmp" && mv "$tmp" config.json

That pattern prevents an invalid command or parse error from replacing your file with an empty file.

Search Deeply with Recursive Descent

Recursive descent, .., walks every value in the JSON tree. Combine it with optional field access when you need to find a key no matter where it appears:

jq '[.. | .id? // empty]' data.json

That is handy for unfamiliar payloads, but it is not a substitute for understanding the structure. On large files, recursive descent can be noisy and slow. Use it to discover the shape, then replace it with a specific path once you know where the data lives.

Validate JSON with jq

Because jq must parse the input before filtering it, this command is a clean syntax check:

jq empty data.json

It prints nothing when the file is valid. It exits non-zero when the file is invalid, which makes it useful in CI:

find . -name '*.json' -not -path './node_modules/*' -print0 |
  while IFS= read -r -d '' file; do
    jq empty "$file" || exit 1
  done

Use How to Validate JSON if you need the broader split between syntax validation, JSON Schema validation, and application-level checks.

Use jq with curl

jq pairs naturally with API calls:

curl -sS https://api.example.com/orders |
  jq -r '.orders[] | select(.status == "failed") | [.id, .customer.name] | @tsv'

For real scripts, keep the HTTP checks outside jq. A 500 error page is not JSON, and jq will only tell you that parsing failed. Save or inspect the raw response when a pipeline unexpectedly starts with a parse error:

curl -fsS https://api.example.com/orders -o response.json
jq '.orders | length' response.json

If the API can return non-JSON error bodies, check status and headers before piping directly into jq.

Common jq Errors and What They Mean

Error What usually happened Fix
parse error: Invalid numeric literal The input is not valid JSON, often HTML or a truncated response. Inspect the raw file with head or validate it with JSON Validator.
Cannot index array with string "name" Your filter expects an object, but the current value is an array. Add .[] or index the array first, such as `.users[]
Cannot iterate over null You used .[] on a missing or null value. Use `.items // []
Cannot index string with string "id" The current value is already a string. Move the field access earlier in the pipeline.
Output still has quotes jq is printing JSON strings. Add -r for raw text output.
Shell says the command is malformed The shell ate part of your jq filter. Wrap the filter in single quotes, and pass values with --arg or --argjson.

When a jq command surprises you, add the simplest possible filter at each step:

jq '.orders' orders.json
jq '.orders[]' orders.json
jq '.orders[] | .customer' orders.json
jq '.orders[] | .customer.name' orders.json

That small-step debugging style beats staring at a 90-character one-liner.

Copy-Paste jq Cheat Sheet

Task Command
Pretty-print JSON jq . file.json
Minify JSON jq -c . file.json
Sort object keys jq -S . file.json
Validate JSON syntax jq empty file.json
Read one field jq '.data.token' response.json
Read raw string jq -r '.data.token' response.json
First array item jq '.[0]' list.json
All array items jq '.[]' list.json
One field from each item `jq -r '.items[]
Filter array rows `jq '.items[]
Filter with fallback `jq '.items[]
Count array items `jq '.items
Sum a field `jq '[.items[].amount]
Sort by field `jq '.items
Build new objects `jq '.items[]
Delete a field jq 'del(.token)' response.json
Export CSV rows `jq -r '.items[]
Pass a string variable `jq --arg id "$id" '.items[]
Pass a numeric variable `jq --argjson min 10 '.items[]

When a Browser Tool Is Faster

jq is the right tool for scripts, CI, repeatable API checks, and large local files. A browser tool is faster when you are doing a one-off visual inspection:

Frequently Asked Questions

How do I pretty-print JSON with jq?

Run jq . file.json. The identity filter . returns the input, and jq formats valid JSON by default. Use jq -c . file.json when you want compact one-line JSON instead.

How do I extract a field with jq?

Use a path filter such as jq '.data.token' response.json. For arrays, iterate first: jq -r '.items[] | .id' response.json.

How do I filter a JSON array with jq?

Iterate with .[] and keep matching rows with select(), for example jq '.orders[] | select(.status == "paid" and .total > 100)' orders.json.

Why does jq output strings with quotes?

jq prints JSON by default, and JSON strings include quotes. Add -r when you want raw text for shell variables, filenames, IDs, CSV pipelines, or clipboard output.

Can jq update a JSON file in place?

No. jq writes the modified JSON to standard output. Save through a temporary file, for example tmp=$(mktemp); jq '.debug = false' config.json > "$tmp" && mv "$tmp" config.json.

How do I pass a shell variable to jq?

Use --arg name "$value" for strings and --argjson name "$json" for numbers, booleans, arrays, or objects. Reference the value inside the filter as $name.

Can jq validate JSON?

Yes. jq empty file.json parses the input, prints nothing when it is valid, and exits non-zero when it is invalid. It validates syntax, not JSON Schema rules.

Why do I get Cannot iterate over null in jq?

You used .[] on a missing or null value. Default the value first with .items // [] | .[], or use optional iteration with .items[]? when missing arrays are acceptable.

Keep Going

Sources

Last reviewed July 2026.