RelicRELIC
Developer cheat sheets

jq cheat sheet

jq is sed for JSON: a small language for slicing and reshaping data on the command line. This cheat sheet walks the filters you use most, built up against one API response so every command has a visible result.

Jordan Gibbs July 10, 2026 8 min read

jq is a command-line tool that reads JSON, runs it through a filter you write, and prints JSON back out. The mental model that makes it click: a jq program is a pipeline. Data enters on the left, each stage transforms it, and what falls out the right is your answer. Once pipes feel natural, the rest of jq is just a vocabulary of filters to drop into that pipeline.

Everything below runs against one response, a short list of users from an imaginary API. Assume it is saved as users.json and piped into jq.

users.json
{
  "users": [
    { "id": 1, "name": "Ada Lovelace",   "role": "admin",  "active": true,  "logins": 42 },
    { "id": 2, "name": "Alan Turing",    "role": "member", "active": true,  "logins": 17 },
    { "id": 3, "name": "Grace Hopper",   "role": "admin",  "active": false, "logins": 8  },
    { "id": 4, "name": "Katherine Johnson", "role": "member", "active": true, "logins": 63 }
  ]
}

Identity and field access

The simplest filter is ., the identity. It passes its input straight through, which is why jq . is the usual way to pretty-print a file. From there, a key after the dot reaches into an object.

identity and a field
jq '.' users.json          pretty-prints the whole document
jq '.users' users.json     returns the array of user objects
jq '.users[0].name' users.json
                           returns: "Ada Lovelace"

Indexing an array uses [n], and a bare [] explodes an array into a stream of its elements, one value at a time. That stream is the input the next stage in a pipe will see.

Pipes

The pipe | feeds the output of one filter into the next, exactly like a shell pipe but inside jq. This lets you drill in stages. The two commands below are equivalent; the piped form reads more clearly as the paths grow.

pipes
jq '.users[0].name' users.json
jq '.users[0] | .name' users.json
                           both return: "Ada Lovelace"

map and select

map(f) applies a filter to every element of an array and collects the results back into an array.select(cond) passes its input through only when the condition holds, and drops it otherwise, which makes it the filter for keeping some elements and discarding the rest.

map: pull one field from each user
jq '.users | map(.name)' users.json
                           returns:
["Ada Lovelace", "Alan Turing", "Grace Hopper", "Katherine Johnson"]

To filter a list, explode it with [], run each element through select, and the stream that survives is your result. This keeps only the admins.

select: keep the admins
jq '.users[] | select(.role == "admin")' users.json
                           returns the Ada and Grace objects

map and select combine cleanly. Inside a map, use select to keep some elements while staying in array form, which is often what you want feeding the next stage.

map + select: names of active users
jq '.users | map(select(.active)) | map(.name)' users.json
                           returns:
["Ada Lovelace", "Alan Turing", "Katherine Johnson"]

keys, values and length

These three answer the everyday structural questions. keyslists an object’s keys, sorted.length counts: the number of elements in an array, keys in an object, or characters in a string.

keys, values, length
jq '.users[0] | keys' users.json
                           returns: ["active", "id", "logins", "name", "role"]

jq '.users | length' users.json
                           returns: 4

jq '.users[0] | .name | length' users.json
                           returns: 12   (characters in "Ada Lovelace")

The companion to keys is [.[]], or the built-in over objects: piping an object to .[] streams its values, so .users[0] | [.[]]collects that user’s values into an array.

Building objects

Curly braces build a new object, and inside them you assign fields from the input. This is how you reshape a fat record down to just the fields you care about, or rename them.

build a trimmed object per user
jq '.users[] | {name: .name, admin: (.role == "admin")}' users.json
                           returns one object per user, e.g.
{ "name": "Ada Lovelace", "admin": true }

jq has a shorthand: when the key matches the field name, {name} means {name: .name}. So {name, role} plucks those two fields straight through. To reach nested data, path into the input on the right of the colon, as in {name: .user.name} for a document one level deeper.

Arrays from streams, and raw output

Wrapping any filter in [ ] gathers its whole output stream into a single array. This is the inverse of [], and it is how you turn a stream of results back into one JSON array.

collect a stream into an array
jq '[.users[] | select(.active) | .name]' users.json
                           returns:
["Ada Lovelace", "Alan Turing", "Katherine Johnson"]

By default jq prints strings with their quotes, which is correct JSON but awkward when you want to pipe a plain value into another shell command. The -r flag (raw output) strips the quotes, giving you bare text, one per line.

-r for raw, shell-friendly output
jq -r '.users[].name' users.json
                           returns bare lines:
Ada Lovelace
Alan Turing
Grace Hopper
Katherine Johnson
One related flag pays off constantly: -c prints compact output, one JSON value per line with no pretty indentation. It pairs well with tools that expect newline-delimited JSON, and with -r when you are feeding results into a loop.

group_by and sort_by

sort_by(f) orders an array by the value a filter returns, and group_by(f) buckets adjacent equal values into sub-arrays. Both expect an array as input, so feed them .users rather than the exploded stream.

sort_by: order users by login count
jq '.users | sort_by(.logins) | map(.name)' users.json
                           returns names, fewest logins first:
["Grace Hopper", "Alan Turing", "Ada Lovelace", "Katherine Johnson"]

group_by returns an array of arrays, one group per distinct value. It sorts before grouping, so you do not have to. Here it splits users by role, then counts each group.

group_by: count users per role
jq '.users | group_by(.role) | map({role: .[0].role, count: length})' users.json
                           returns:
[{ "role": "admin", "count": 2 }, { "role": "member", "count": 2 }]

Defaults and missing fields

Real API responses have holes: a field is sometimes absent, sometimes null. Accessing a missing key in jq yieldsnull rather than an error, which is forgiving, but null values then flow downstream and can break a later stage. The alternative operator // supplies a fallback when the left side is null or false.

// supplies a default
jq '.users[] | .nickname // .name' users.json
                           uses nickname when present, otherwise name;
                           here every user lacks nickname, so it returns each name

To test for a key before touching it, has("key") returns true or false, and the ? suffix on a path makes an operation quietly skip inputs it cannot apply to instead of erroring. Both are the tools for surviving inconsistent data without a wall of error output.

Turning objects into pairs

Sometimes you need to iterate an object’s keys and values together, or rewrite its shape. to_entries converts an object into an array of {key, value} pairs; from_entries reverses it. This is the standard idiom for transforming every field of an object.

to_entries: list an object as pairs
jq '.users[0] | to_entries | map(.key)' users.json
                           returns:
["id", "name", "role", "active", "logins"]

Between the two you can remap keys, drop fields conditionally, or filter an object down, all with the array tools you already know from earlier. It is the escape hatch for object work that map alone cannot reach, because map operates on arrays.

A worked example, start to finish

Put the pieces together. Suppose you want the names of active members, in order of most logins, as plain lines. Read the pipeline left to right: take the users, keep the active members, sort by logins descending, pull the names, print raw.

one pipeline, several stages
jq -r '.users
  | map(select(.active and .role == "member"))
  | sort_by(-.logins)
  | .[].name' users.json

                           returns:
Katherine Johnson
Alan Turing

That is the whole idiom of jq: small filters, joined by pipes, each doing one thing. Note sort_by(-.logins) sorts by the negated login count, which is the simplest way to get descending order for a number.

jq is picky about quoting on the command line. The filter goes in single quotes so your shell leaves it alone, and string literals inside the filter use double quotes, as in select(.role == "admin"). If a jq program fails with a cryptic parse error, a quote the shell ate is the first thing to check. When the payload is messy to read, format it first: our JSON formatter and JSON validator both run locally in the browser.

Where to go next

jq goes much deeper, with variables, functions, reduce and string interpolation, but the filters above cover the large majority of day-to-day use. If you would rather point at values than transform them, the sibling JSONPath examples cover the selection-only approach, and for the pattern-matching that often sits alongside jq, the cluster pillar is the regex cheat sheet. Living in a shell all day also means copying commands cleanly; our guide to copy and paste in the terminal covers the shortcuts that will not cancel the command you just pasted.

Written by
Jordan GibbsFounder, Relic

Jordan Gibbs is the founder of Relic, an end-to-end encrypted, permanent, searchable memory for everything you copy. He writes widely about AI, agents, and practical tooling on Medium, where he is read by tens of thousands, and builds privacy-first software. Here he covers how everyday tools like the clipboard actually work, and how to use them without handing your data to someone else.

MediumGitHubLinkedIn
Part of
Developer cheat sheets
Keep reading
Pillar·9 min

Regex cheat sheet

The regex pieces you actually reach for, character classes through lookarounds, with the flavor differences that bite you and a handful of patterns worth saving.

Read
8 min

JSONPath examples

JSONPath is XPath for JSON. One small store document, threaded through wildcards, slices, filters and recursive descent, with the honest note on how implementations vary.

Read
8 min

wget vs curl: what is the difference?

wget and curl both fetch over HTTP, and there the resemblance ends. One is a recursive downloader, the other a protocol swiss army knife. What each is genuinely better at.

Read
developerjsonreference