jq Cheat Sheet

jq is a small functional language for slicing and reshaping JSON. This cheat sheet collects the filters and flags you reach for most, with recipes for everyday tasks. Each example works with jq 1.7 or later.

How jq thinks

jq reads JSON from files or standard input and writes JSON (or raw text) to standard output, so it slots into shell pipelines next to curl, kubectl, aws and gh, all of which can emit JSON. It has no dependencies, ships as a single binary, and is available from every common package manager.

A jq program is a filter: it takes one JSON value in and produces zero or more values out. Filters are combined with | (feed the output of the left side into the right side) and , (produce the outputs of both). When a filter produces several values, everything after the next pipe runs once for each of them. Wrap an expression in [ ... ] to collect its outputs into a single array.

jq '.' data.json                 # pretty-print
jq -c '.items[]' data.json       # one compact line per item
curl -s https://api.example.com/orders | jq '.data | length'

Paths and iteration

.                     # the whole input
.name                 # a field
.customer.email       # nested field
."first name"         # keys that are not identifiers
.tags[0]              # first array element
.tags[-1]             # last element
.items[2:5]           # slice (end is exclusive)
.items[]              # every element, one output each
.items[]?             # same, but no error if .items is not iterable
.address?.city        # ? suppresses errors on the left
..                    # every value at every depth (recursive descent)

Accessing a missing key returns null rather than an error. Indexing something that is not an object or array, such as .name on a number, is an error unless you add ?.

Selecting and transforming

.items[] | select(.qty > 1)                  # keep matching values
map(select(.paid))                           # filter an array, keep it an array
map(.price * .qty)                           # transform each element
map(.price * .qty) | add                     # sum
[.items[].sku] | unique                      # distinct values
sort_by(.created) | reverse                  # newest first
group_by(.status) | map({status: .[0].status, count: length})
min_by(.price), max_by(.price)
length                                       # array/object/string length
keys, has("email"), to_entries, from_entries
with_entries(select(.value != null))         # drop null fields
any(.items[]; .qty > 5), all(.items[]; .paid)
first(.items[]), limit(3; .items[])

select(cond) passes its input through when the condition is true and produces nothing otherwise, which is why it is almost always used inside map or after []. The choice between the two forms matters for the shape of the output: map(select(...)) returns one array, ready for length or further array functions, while .[] | select(...) returns separate values, which is what you want when printing one result per line.

Building and updating JSON

{id, name}                         # shorthand for {id: .id, name: .name}
{id: .id, total: (.items | map(.price) | add)}
{(.key): .value}                   # computed key
[.items[] | {sku, qty}]            # reshape an array
.status = "shipped"                # set a value (whole input is output)
.items[].qty |= . + 1              # update in place
.price *= 1.1
del(.password, .token)             # remove keys
.a + .b                            # shallow merge of two objects
.defaults * .overrides             # recursive (deep) merge
.name // "unknown"                 # default when null or false
if .qty > 0 then "in stock" else "sold out" end

= sets a path to a value computed from the original input, whereas |= computes the new value from the old value at that path. That is why .qty |= . + 1 increments but .qty = . + 1 would try to add 1 to the whole object.

Strings, formats and dates

"\(.first) \(.last)"               # string interpolation
ascii_downcase, ascii_upcase
split(","), join(", ")
test("^ord_"), test("error"; "i") # regex match (flags as 2nd arg)
capture("(?<y>\\d{4})-(?<m>\\d{2})")
sub("\\s+$"; ""), gsub("-"; "_")
startswith("http"), ltrimstr("v")
tostring, tonumber
.[] | [.id, .name, .total] | @csv  # CSV row (use with -r)
@tsv, @json, @base64, @base64d, @uri, @html, @sh
now | todate                       # current time as ISO 8601
.created | fromdate                # ISO 8601 string to Unix seconds

Inside a jq string literal a regex backslash must itself be escaped, so a digit class is written "\\d" in the program text.

Variables, reduce and arguments

. as $root | .items[] | {sku, order: $root.id}
reduce .items[] as $i (0; . + $i.qty * $i.price)
paths(type == "number")            # paths to every number
getpath(["a","b"]), setpath(["a","b"]; 1)
$ENV.HOME, env.USER
jq --arg id "ord_42" '.[] | select(.id == $id)' orders.json
jq --argjson min 100 'map(select(.total >= $min))' orders.json

--arg always passes a string; --argjson parses the value as JSON, so use it for numbers and booleans.

Recipes, explained

Count items by status. group_by(.status) | map({status: .[0].status, count: length}) sorts the array into groups with equal status, then turns each group into a small object. Because group_by sorts, the output is ordered by status name; add | sort_by(-.count) to put the largest group first.

Export a table to CSV. Run with -r and finish with @csv: .orders[] | [.id, .customer.name, .total] | @csv. Each array becomes one correctly quoted line. To add a header, emit it first: ["id","name","total"], (.orders[] | [.id, .customer.name, .total]) | @csv.

Find every object that has a particular key, at any depth. .. | objects | select(has("token")) walks the whole document with .., keeps only objects, and keeps those containing the key. This is a quick way to locate credentials before you share a payload.

Turn an object into a list of pairs and back. to_entries converts {"a":1} into [{"key":"a","value":1}], which you can filter or sort like any array; from_entries reverses it. with_entries(f) is shorthand for to_entries | map(f) | from_entries, handy for renaming or dropping keys.

Merge configuration files. jq -s '.[0] * .[1]' base.json override.json slurps both files into an array and deep-merges the second into the first. Arrays are replaced, not concatenated, which is usually what you want for overrides.

Pull one value into a shell variable. id=$(jq -r '.data.id' response.json). Combine with -e if the script should fail when the field is missing, since a missing field otherwise prints the word null.

Error messages you will meet

  • Cannot index array with "name" — you asked for a field on an array. Add [] to iterate first: .items[].name instead of .items.name.
  • Cannot iterate over null — the path before [] does not exist in at least one input. Use []? or a default: (.items // [])[].
  • object (...) and number (...) cannot be added — an arithmetic operator received the wrong types, often because . was not what you expected at that point in the pipe. Insert | debug or split the program to inspect intermediate values.
  • ... is not defined — a misspelled function name, or a function that only exists in a newer jq release.
  • syntax error, unexpected INVALID_CHARACTER — usually shell quoting: the shell has eaten or altered quotes before jq saw the program.

When a program misbehaves, build it up one pipe stage at a time. Every prefix of a jq pipeline is itself a valid program, so you can run .orders, then .orders[], then .orders[] | select(.paid) and watch the data change at each step.

Command-line flags

  • -r raw output: strings without quotes (needed for @csv, @tsv and shell use)
  • -c compact output: one value per line, ideal for JSON Lines
  • -s slurp: read all inputs into one array
  • -n null input: run the program without reading input
  • -e set the exit status from the last output (false or null fails), handy in scripts
  • -S sort object keys; --tab or --indent n for indentation
  • -j like -r but without a newline after each output

Quoting differs by shell. On macOS and Linux, wrap the program in single quotes. In Windows cmd.exe, single quotes are not quote characters, so put the program in double quotes and escape inner double quotes, or save the program to a file and run jq -f filter.jq.

PasteKit can run jq against pasted JSON in the browser using jq compiled to WebAssembly, with large integers kept exact. For path-style lookups without jq’s language, see the JSONPath cheat sheet.

Frequently asked questions

How do I get values without quotes in jq?

Use -r (raw output). Strings are printed without surrounding quotes or escaping, which is what you want for shell variables and CSV.

How do I convert a JSON array to CSV with jq?

Run jq -r ‘.[] | [.id, .name] | @csv’ file.json. Each row becomes a properly quoted CSV line; add a header row with a separate string output first.

What is the difference between = and |= in jq?

With =, the right side is evaluated against the original input. With |=, it is evaluated against the current value at the path, so .n |= . + 1 increments n.

Why does jq round my large numbers?

Versions before 1.7 convert every number to a double. jq 1.7 and later keep the original digits of numbers that a program does not modify.

How do I filter an array but keep it as an array?

Use map(select(condition)). Writing .[] | select(condition) produces separate values instead of one array.

Related