jq Cheat Sheet: Complete Quick Reference for JSON on the Command Line
Last updated:
This is a reference card for jq — the command-line JSON processor — built for scanning, not for reading start to finish. Every section below is a lookup table: operator on the left, what it does in the middle, copy-paste example on the right. If you want a walkthrough with worked examples and longer recipes, see our jq examples deep dive instead. This page is what you bookmark and grep for the syntax you forgot at 2 AM. Coverage is jq 1.7+ (June 2023 and newer), which is what every current package manager ships. Older 1.6 is still common in long-running CI images — most of what is here works identically, with a few late-additions (abs, toarray) flagged where relevant.
Building a jq filter against a JSON file that might be broken? Paste it into Jsonic's JSON Validator first — it catches trailing commas, missing quotes, and bracket mismatches with line numbers so you do not waste time debugging a filter against malformed input.
Validate JSON InputIdentity, dot, and pipe: jq fundamentals
Every jq filter starts from a single concept: an expression takes an input value and produces a stream of output values. The dot . is the identity expression — it outputs whatever came in. The pipe | feeds the output of the left expression as input to the right expression. Almost every filter you will ever write is a chain of pieces joined by pipes.
| Syntax | What it does | Example |
|---|---|---|
. | Identity — output input unchanged (pretty-prints by default) | jq . file.json |
| | Pipe — feed left output into right expression | .users[] | .name |
, | Comma — produce two values from one input | .name, .email |
() | Grouping — control evaluation order | (.a + .b) * .c |
$__loc__ | Source location for debugging | $__loc__ |
// | Alternative — left if non-null/non-false, else right | .name // "anonymous" |
? | Suppress errors from the preceding filter | .user.age? |
.. | Recursive descent — emit every value in the tree | .. | numbers |
The pipe is the most-used operator. Read .users[] | select(.active) | .email left to right: take users, iterate it, keep only active ones, then project the email. The comma is the second-most-used: it forks the stream, producing two outputs per input rather than one. Default-fallback with //is the jq equivalent of JavaScript's ?? — return the right side when the left is null or false.
# Read a file and pretty-print it
jq . users.json
# Project a single field from every element
curl -s https://api.example.com/users | jq '.[].email'
# Default value when a key may be missing
jq '.config.timeout // 30' settings.jsonObject access: .foo, .foo.bar, .foo? safe navigation
Field access uses the dot. Plain dot-name fails loudly when the input is not an object or when the field type does not match what the next operator expects. Question-mark variants suppress the error and substitute null, which is what you want when the input shape is irregular.
| Syntax | What it does | Example |
|---|---|---|
.foo | Access field foo — error if input is not an object | .name |
.foo.bar | Chained access through nested objects | .user.profile.email |
.foo? | Same as .foo but returns null on type mismatch | .user? |
."foo bar" | Access field with spaces or special characters | ."content-type" |
.["foo"] | Bracket form — works with any string, also for dynamic keys | .["x-api-key"] |
has("foo") | Boolean test for key presence | select(has("email")) |
keys | Sorted array of object keys | .config | keys |
keys_unsorted | Keys in insertion order | .config | keys_unsorted |
to_entries | Convert object to [{key, value}] array | .headers | to_entries |
from_entries | Inverse of to_entries | map({key:.k, value:.v}) | from_entries |
The to_entries / from_entries pair is how you transform object structure. To rename every key to lowercase: with_entries(.key |= ascii_downcase) (which is sugar for the entries round-trip). To filter object keys by name pattern: with_entries(select(.key | startswith("x-"))).
# Safe deep access — never errors, returns null for missing paths
jq '.user?.profile?.email // "unknown"' input.json
# Dynamic key from a variable
jq --arg field "email" '.[$field]' user.json
# Filter object to only x- prefixed headers
jq 'with_entries(select(.key | startswith("x-")))' headers.jsonArray operations: .[], .[0], .[N:M] slicing, length
Arrays are accessed by index, iterated as streams, or sliced. Negative indices count from the end. Slices use Python-style [start:end] semantics, where end is exclusive.
| Syntax | What it does | Example |
|---|---|---|
.[0] | First element | .users[0] |
.[-1] | Last element | .users[-1] |
.[2:5] | Slice from index 2 (inclusive) to 5 (exclusive) | .users[0:10] |
.[] | Iterate — output each element as its own value | .users[] | .name |
.[]? | Iterate, but no error if input is not iterable | .users[]? |
length | Element count (or string length, or 0 for null) | .users | length |
first / last | First or last element of an array stream | [range(10)] | last |
reverse | Reverse element order | .scores | reverse |
flatten | Flatten nested arrays one level (or N with arg) | .matrix | flatten |
range(N) | Stream of integers 0 to N-1 | [range(5)] |
The crucial distinction is stream vs array. .users[] produces a stream of N values; .users alone produces one array value. Most built-ins expect an array — to collect a stream back, wrap in brackets: [.users[] | .name] gives you an array of names. Conversely, when you want the array's length, do not iterate first: .users | length is correct; .users[] | length gives the length of each element (wrong if you wanted the count).
# Count elements
jq '.users | length' data.json
# Take first 10 elements
jq '.users[0:10]' data.json
# Get the last entry from a log
jq '.events | last' log.json
# Iterate, project, collect back to array
jq '[.users[] | .email]' data.jsonFilters and selectors: select(), map(), filter patterns
select() is the keep-or-drop filter — it passes its input through when the condition is true and emits nothing otherwise. map() is the array transformer — it applies a filter to each element of an array and returns a new array. Together they cover most filtering work.
| Pattern | What it does | Example |
|---|---|---|
select(cond) | Pass through if cond is true, drop otherwise | .[] | select(.age > 30) |
map(f) | Apply f to each array element, return array | map(.name) |
map(select(c)) | Filter array elements, return array | map(select(.active)) |
any(c) / all(c) | Does any/all element satisfy c? | any(.active) |
contains(v) | Recursive substring/subset match | contains({tags: ["urgent"]}) |
IN(stream) | Is value in the stream? (jq 1.6+) | .role | IN("admin","owner") |
not | Boolean negation (postfix) | select(.active | not) |
and / or | Boolean combinators (short-circuit) | select(.age > 18 and .country == "US") |
== / != | Equality / inequality | select(.status == "active") |
< > <= >= | Ordering — works on numbers, strings, arrays | select(.score >= 80) |
Two common patterns are worth memorizing. To filter an array and keep it as an array: map(select(.active)). To filter a stream and re-collect: [.[] | select(.active)]. They produce the same output; pick whichever reads better in your pipeline. The IN() built-in is much faster than chained equality when matching against a set of allowed values.
# Filter users with admin or owner role
jq '.users | map(select(.role | IN("admin","owner")))' data.json
# Find any user with unverified email
jq '.users | any(.verified | not)' data.json
# Combined filter with two conditions
jq '.events[] | select(.severity == "error" and .ts > 1700000000)' log.jsonType checks: numbers, strings, booleans, null, type-coercion
jq has six JSON types: null, boolean, number, string, array, object. The type built-in returns the type as a string; type-named filters (numbers, strings) let only matching values through.
| Filter | What it does | Example |
|---|---|---|
type | Type name as string | .value | type |
numbers | Pass only numbers, drop everything else | .. | numbers |
strings | Pass only strings | .. | strings |
booleans | Pass only true/false | .. | booleans |
nulls | Pass only null | .. | nulls |
arrays | Pass only arrays | .. | arrays |
objects | Pass only objects | .. | objects |
tostring | Coerce to JSON string | .id | tostring |
tonumber | Parse string to number — errors on non-numeric | .port | tonumber |
isnan / isinfinite | Number health checks | select(.x | isnan | not) |
The recursive descent .. combined with a type filter is the recipe for finding every value of a given type anywhere in the tree. .. | numbers emits every number, no matter how deep it is nested. Add [...] to collect them all: [.. | numbers] is a flat array of every number in the document — useful for stats, validation, or hunting magic numbers.
# Collect every string in the tree
jq '[.. | strings]' deep.json
# Coerce numeric IDs that were stringified by an upstream system
jq '.users[] | .id |= tonumber' data.json
# Drop entries where score is NaN
jq '.records | map(select(.score | isnan | not))' data.jsonAggregation: add, max, min, group_by, sort_by, unique_by
The aggregation built-ins reduce a stream or array to a single value or rearrange an array. add sums numbers, concatenates strings, or merges objects, based on element type. group_by partitions by a key. sort_by orders ascending by a key (use - in the argument to descend).
| Filter | What it does | Example |
|---|---|---|
add | Sum numbers, concat strings/arrays, merge objects | [.users[].age] | add |
min / max | Smallest / largest element by natural order | .scores | max |
min_by(f) / max_by(f) | Element with smallest/largest f value | .users | max_by(.score) |
sort | Sort ascending by natural order | .scores | sort |
sort_by(f) | Sort by computed key | .users | sort_by(.created_at) |
sort_by(-f) | Sort descending (negate numeric key) | sort_by(-.score) |
group_by(f) | Partition into groups by key | .users | group_by(.role) |
unique | Sort and deduplicate | .tags | unique |
unique_by(f) | Deduplicate by computed key | .users | unique_by(.email) |
reduce | Fold a stream with an accumulator | reduce .[] as $x (0; . + $x.n) |
group_by returns an array of arrays — one inner array per group. To compute a per-group aggregate, pipe through map: group_by(.role) | map({role: .[0].role, count: length}) turns a flat user list into a role-count summary.
# Total spend across all transactions
jq '[.transactions[].amount] | add' data.json
# Top user by score
jq '.users | max_by(.score)' data.json
# Group users by role, count each group
jq '.users | group_by(.role) | map({role: .[0].role, count: length})' data.json
# Sort by date descending (newest first)
jq '.events | sort_by(.created_at) | reverse' log.jsonString manipulation: split, join, ascii_downcase, gsub, test
String functions cover splitting, joining, case conversion, substring tests, and regex. test is the regex-match predicate; match returns full match details; gsub does global substitution.
| Filter | What it does | Example |
|---|---|---|
split(s) | Split string on separator into array | .csv | split(",") |
join(s) | Join array of strings with separator | .tags | join(", ") |
ascii_downcase / ascii_upcase | ASCII-only case conversion (fast) | .email |= ascii_downcase |
ltrimstr(s) / rtrimstr(s) | Strip prefix / suffix if present | .url | ltrimstr("https://") |
startswith(s) / endswith(s) | Boolean prefix/suffix test | select(.path | startswith("/api")) |
contains(s) | Substring test (also works on arrays/objects) | select(.message | contains("error")) |
test(re) | Regex match — returns boolean | select(.email | test("@.+\\\\.")) |
match(re) | Full match info (offset, length, captures) | .line | match("(\\\\d+)") |
gsub(re; rep) | Global substitution — regex-based | .text | gsub(" +"; " ") |
sub(re; rep) | Replace first match only | .url | sub("^http:"; "https:") |
@uri / @csv / @tsv | Format directives — URL/CSV/TSV encoding | @uri "\\(.query)" |
String interpolation uses backslash-paren: "User \(.name) <\(.email)>" substitutes filter results into a string. Combine with -r for plain-text output ready to pipe to other commands. The @csv and @tsv directives are the safe way to produce delimited output — they handle quoting and escaping that you would otherwise have to roll by hand.
# Lowercase all emails
jq '(.users[].email) |= ascii_downcase' data.json
# Strip prefix from URLs
jq '.links[] | ltrimstr("https://")' data.json
# Find log lines matching a regex
jq -r '.[] | select(.msg | test("timeout|refused"))' log.json
# Produce CSV from an array of objects
jq -r '.users[] | [.id, .name, .email] | @csv' data.jsonOutput formatting: -r raw, -c compact, --tab, --slurp, --arg
jq has more command-line flags than filter operators. These control how input is read, how output is rendered, and how external values are passed in. Memorize the top five and most jq invocations will write themselves.
| Flag | What it does | When to use |
|---|---|---|
-r / --raw-output | Strip quotes from string output | Piping to grep, cut, awk, xargs, or shell var |
-c / --compact-output | One JSON value per line, no pretty-print | Producing NDJSON / JSON Lines |
--tab | Indent with tabs instead of two spaces | Personal preference / diff readability |
--indent N | Use N spaces of indentation | Match repo formatter |
-s / --slurp | Read entire input stream into one array | Aggregating across NDJSON records |
-n / --null-input | Run with null input — no stdin read | Generating JSON from scratch with --arg |
-R / --raw-input | Read input as raw text, not JSON | Parsing non-JSON log files |
--arg name value | Bind shell value as jq string variable | Pass user input safely |
--argjson name value | Bind shell value as JSON-typed variable | Pass numbers, booleans, JSON arrays |
--rawfile name path | Load file contents into a string variable | Embed a template or key file |
--slurpfile name path | Load file contents as JSON value | Reference data alongside main input |
-e / --exit-status | Exit non-zero if no output or last is null/false | Use jq as a predicate in shell scripts |
The two most common patterns: jq -r '.users[] | "\(.id) \(.name)"' for shell-friendly tabular output, and jq -c '.[]' to convert a JSON array into NDJSON for piping line-by-line to tools that expect one record per line. The exit-status flag turns jq into a real predicate: jq -e '.success' resp.json && echo OK.
# Convert JSON array to NDJSON for streaming
jq -c '.[]' big.json > stream.ndjson
# Slurp NDJSON back into a single array
jq -s '.' stream.ndjson > big.json
# Generate JSON from shell variables
jq -n --arg name "Ada" --argjson age 36 '{name: $name, age: $age}'
# Use jq as a predicate in a shell condition
if jq -e '.errors | length == 0' response.json > /dev/null; then
echo "no errors"
fiKey terms
- filter
- A jq expression — the program you write between quotes on the command line. Every filter takes an input value and produces a stream of zero or more output values.
- identity filter
- The single dot
.— outputs the input unchanged.jq . file.jsonuses it to pretty-print JSON because the default output is indented. - pipe
- The
|operator inside a jq filter (not the shell pipe). It feeds the output of the left expression as input to the right, the same way the shell pipe feeds stdout to stdin. - stream
- A sequence of JSON values produced by an expression.
.[]turns an array into a stream of its elements;[...]collects a stream back into an array. - raw output
- The
-rflag — strips the surrounding quotes from string output values, producing plain text suitable for shell consumption. Without it, jq emits valid JSON that downstream JSON tools can read. - slurp
- The
-sflag — reads the entire input stream into a single jq array before applying the filter. Used when input is multiple top-level JSON values (such as NDJSON) and you want to operate on the whole batch. - recursive descent
- The
..operator — emits every value in the input tree (the input itself, then every nested value at every depth). Combine with a type filter to find values anywhere in the structure.
Frequently asked questions
What does jq actually do?
jq is a small command-line program (a single binary, written in C, no runtime) that reads JSON from standard input, applies a filter expression you write on the command line, and writes JSON or plain text to standard output. The filter language is a tiny domain-specific language built around the identity dot, the pipe, field access, array iteration, and a library of built-in functions. A typical invocation looks like curl https://api.example.com/users | jq '.[].email' — fetch JSON, then print the email field of every element. jq is the tool you reach for when you need to extract, transform, filter, or reshape JSON in a shell pipeline without writing a Python or Node script. It is bundled with most Linux distributions, ships in Homebrew on macOS, and has Windows binaries. Version 1.7 (June 2023) is the current line and is what most package managers install today.
How do I install jq?
On macOS use Homebrew: brew install jq. On Debian, Ubuntu, and derivatives: sudo apt install jq. On Fedora and RHEL-family distributions: sudo dnf install jq. On Arch Linux: sudo pacman -S jq. On Windows the cleanest options are Chocolatey (choco install jq) or Scoop (scoop install jq), both of which add jq to PATH automatically. On Alpine in containers: apk add --no-cache jq. If you cannot use a package manager, download the static binary from the jq GitHub releases page — it has zero runtime dependencies, so dropping the binary into /usr/local/bin or anywhere on PATH is enough. Verify the install with jq --version, which should report jq-1.7 or newer. Older 1.6 binaries still work for most cases but lack a handful of newer functions like abs and toarray; if your filter does not use those, the version difference is invisible.
What's the difference between . and .[]?
The single dot . is the identity filter — it passes the input through unchanged and is what jq uses to pretty-print JSON when you do not specify a filter. The dot-bracket .[] is the iteration filter — it takes an array or object as input and outputs each element (for arrays) or each value (for objects) as a separate JSON value on its own line. The distinction matters because jq is stream-oriented. Given an array of three users, . outputs one array containing three objects; .[] outputs three separate objects, one after another. That stream of values is what subsequent filters see. .[].name therefore means "iterate the array, then get the name field of each element". To collect a stream back into an array, wrap the expression in square brackets: [.[].name] turns the three-name stream into a three-element array.
How do I select multiple fields at once?
Three patterns cover almost every case. First, comma — .name, .email outputs name and email as separate values on separate lines. This is the right shape for shell loops but breaks the JSON-per-line invariant you may want downstream. Second, object construction — {name, email} or the explicit {name: .name, email: .email} produces a single JSON object containing only the selected fields, which is what you want when piping to another JSON consumer. Third, array construction — [.name, .email] produces a single JSON array with the two values, useful when you need positional access. Combined with .[] you get patterns like .users[] | {id, name, email} — iterate users, project each into a three-field object. For shell consumption you often want raw output too: jq -r '.users[] | "\(.name)\t\(.email)"' uses string interpolation to produce TSV output ready for cut, sort, or awk.
How do I filter array elements by a condition?
Use select() with the iteration filter. The pattern is .[] | select(condition) — iterate the array, then for each element pass it through only if the condition is true. Conditions use the standard comparison operators: ==, !=, <, <=, >, >=, and the logical operators and, or, not. Examples: .[] | select(.age > 30) keeps elements where age exceeds 30; .[] | select(.status == "active") keeps active records; .[] | select(.tags | contains(["urgent"])) keeps elements whose tags array contains "urgent". To collect filtered elements back into an array, wrap the whole expression: [.[] | select(.active)]. The map(select(...)) shorthand is equivalent and reads better when the rest of the filter chain uses map style: map(select(.score > 80)) | sort_by(-.score) keeps high scores then sorts descending.
How do I pass shell variables into jq?
Use --arg for strings and --argjson for any other JSON type. The pattern is jq --arg name "$SHELL_VAR" '.users[] | select(.name == $name)' — the flag binds the shell value to a jq variable named $name (the dollar sign is part of how you reference it inside the filter, not part of the flag). Use --arg when the value is a string; jq will treat it as a JSON string regardless of what it looks like. Use --argjson when you need a number, boolean, null, array, or object: jq --argjson minAge 18 '.[] | select(.age >= $minAge)' passes the integer 18, not the string "18". Multiple values: chain the flags — jq --arg env "prod" --argjson port 8080 '.config'. Never interpolate shell variables into the filter string itself with quotes — you reintroduce shell-injection surface and break on values containing apostrophes or backslashes.
What's the difference between -r and default output?
By default jq emits JSON-encoded output: strings come back with surrounding quotes and escaped special characters, numbers are unquoted, objects and arrays are pretty-printed across multiple lines. The -r flag (raw output) strips the surrounding quotes from string values and decodes JSON escapes, producing plain text. Compare jq '.name' on input {"name":"Ada Lovelace"} — default output is "Ada Lovelace" with quotes (still a JSON string), -r output is Ada Lovelace with no quotes (a bare shell value). Use -r whenever the next step is a shell command that expects raw text — cut, grep, awk, xargs, or assigning to a shell variable. Use the default when the next step is another JSON consumer, because -r breaks JSON validity for any non-string output it happens to produce. Pair -r with --tab or string interpolation for clean TSV: jq -r '.[] | [.id, .name] | @tsv' is the canonical jq-to-TSV pipeline.
How do I update a JSON value with jq?
Use the assignment operators. The pipe-equals |= updates a value in place by applying a filter to it: .age |= . + 1 increments the age field; .name |= ascii_downcase lowercases the name. The plain equals = sets a value from the root: .status = "active" sets status to a literal string regardless of input. For deeper paths the rules are the same: .user.profile.email |= ascii_downcase updates a nested string. To add a new field: . + {created: now} merges the new key in. To delete a field: del(.password) removes the path. To update multiple fields: .name |= ascii_downcase | .email |= ascii_downcase chains updates with the pipe operator. jq emits the modified JSON to stdout — it does not edit files in place. To overwrite a file the idiom is jq '.field = "value"' input.json > tmp.json && mv tmp.json input.json, or use sponge from moreutils: jq '...' input.json | sponge input.json.
Further reading and primary sources
- jq Manual (Official) — Authoritative reference for every operator, built-in, and command-line flag — the canonical source
- jq GitHub Releases — Download static binaries for any platform — no runtime dependencies
- jqplay — online jq REPL — Paste input and filter, see output instantly — the fastest way to iterate on a complex filter
- jq Wiki: FAQ and Cookbook — Community-curated recipes for common transformations and edge cases
- Walkthrough: jq examples deep dive — Long-form companion to this cheat sheet with worked examples and recipes
- yq vs jq comparison — When to use yq (YAML, drop-in jq syntax) versus jq (JSON only) — sibling tools
- Parse JSON in Bash — Beyond jq: pure-bash patterns and when jq is the right answer anyway
- Format JSON on CLI — Pretty-print options across jq, python -m json.tool, prettier, and editor formatters
- JSON with curl — Sibling page — curl flags for sending and receiving JSON, the natural upstream of any jq pipeline
- jq vs JSONPath — Two query languages, different mental models — when each fits