cheat sheet

jq

Slice, filter, map, and transform JSON data from the command line. Covers all essential filters, built-in functions, select, map, reduce, streaming, jq 1.7/1.8 additions, and real-world API response processing.

jq — JSON Processor

What it is

jq is a free, open-source command-line JSON processor written in C and maintained at jqlang.org. It provides a lightweight functional language for slicing, filtering, mapping, and transforming JSON data from files or stdin, with no runtime dependencies beyond the binary itself. Reach for jq whenever you need to extract fields from API responses, reshape JSON for other tools, or pretty-print raw JSON in a shell pipeline; for very large JSON streams, consider jq's streaming mode or a tool like fx for interactive exploration.

The project ended a five-year release gap with jq 1.7 (Sept 2023), followed by 1.7.1 (Dec 2023) and 1.8.0 (June 2024). Recent releases brought pick, INDEX/IN/JOIN, trim/ltrim/rtrim, abs, toboolean, --raw-output0, JQ_COLORS/NO_COLOR, and switched string indexing builtins from bytes to Unicode code points — see What's new in jq 1.7 / 1.8 below.

Installation

bash
sudo apt install jq
sudo dnf install jq
brew install jq

Output: (none — exits 0 on success)

Syntax

A jq invocation takes zero or more options, a single filter expression (a string), and one or more input files or stdin. Every filter is applied to each top-level JSON value in the input, and results are pretty-printed to stdout by default.

bash
jq [OPTIONS] FILTER [FILE...]
cat data.json | jq FILTER
curl -s https://api.example.com | jq '.'

Output: (none — exits 0 on success)

Essential options

OptionMeaning
-rRaw output (unquoted strings)
-cCompact output (no pretty print)
-nNull input (create JSON without stdin)
-eExit 1 if last output is false/null
-sSlurp: read all inputs into an array
-RRaw input: treat input as raw strings
-jJoin output (like -r but no trailing newline)
--arg NAME VALPass shell variable as string
--argjson NAME VALPass shell variable as JSON
--slurpfile NAME FILELoad file as JSON into variable
--tabUse tabs for indentation
--indent NUse N spaces
--raw-output0NUL-separate raw outputs (jq 1.7+) — pairs with xargs -0
-L DIR / --library-path DIRAdd jq module search path (long form added in 1.8)
JQ_COLORS envCustomise output colors; truecolor escapes accepted in 1.8
NO_COLOR envDisable colored output (jq 1.7+, honours no-color.org)

Basic filters

The identity filter . passes input through unchanged (useful for pretty-printing). Field access uses .key for objects and .[N] for arrays; .[] iterates over every element and outputs each as a separate value. Appending ? to any filter suppresses errors when the path does not exist.

bash
jq '.'                       # pretty print (identity)
jq '.key'                    # get object field
jq '.key.nested'             # nested field
jq '.key?'                   # optional (no error if missing)
jq '.[0]'                    # array index (0-based)
jq '.[-1]'                   # last element
jq '.[2:5]'                  # array slice (indices 2,3,4)
jq '.[]'                     # iterate over array/object values
jq 'keys'                    # array of object keys
jq 'values'                  # array of object values
jq 'length'                  # length of array/string/object
jq 'type'                    # "null","boolean","number","string","array","object"

Output: (none — exits 0 on success)

Given {"name":"Alice","age":30,"city":"NYC","scores":[95,87,92],"active":true}:

Output:

text
{
  "name": "Alice",
  "age": 30,
  "city": "NYC",
  "scores": [95, 87, 92],
  "active": true
}

jq '.name'"Alice" · jq '.[0]' on [95,87,92]95 · jq 'keys'["active","age","city","name","scores"] · jq 'length' on the scores array → 3 · jq 'type'"object"

Comma and pipe

Comma (,) runs two filters against the same input and concatenates their outputs. The pipe (|) passes the output of the left filter as input to the right, enabling step-by-step transformations identical in spirit to Unix shell pipes.

bash
jq '.name, .age'             # output multiple fields (separate lines)
jq '.items | length'         # pipe: get array length
jq '.items[] | .name'        # iterate items, get name field
jq '.data | .[] | select(.active)'  # chain

Output: (none — exits 0 on success)

Given {"name":"Alice","age":30} and an array [{"name":"Alice","dept":"Eng"},{"name":"Bob","dept":"Ops"}]:

Output:

text
"Alice"
30

jq '.[] | .name' on the array:

text
"Alice"
"Bob"

Object and array construction

Wrap a filter in {} to build a new JSON object, and in [] to collect multiple outputs into an array. Inside {}, shorthand {foo} expands to {foo: .foo}; wrap the key in parentheses to evaluate it as an expression.

bash
jq '{name: .name, age: .age}'           # build new object
jq '{(.key): .value}'                   # dynamic key from expression
jq '[.items[] | .id]'                   # build array from iteration
jq '[ .[] | select(.score > 50) ]'      # array of filtered elements
jq '{"total": (.items | length)}'       # computed field

Output: (none — exits 0 on success)

Given {"name":"Alice","age":30,"city":"NYC"}:

Output:

text
{
  "name": "Alice",
  "age": 30
}

select — filtering

select(condition) passes its input through unchanged when the condition is truthy, and produces no output otherwise. It is the primary way to filter elements out of an iteration — chain it after .[] to keep only matching items.

bash
jq '.[] | select(.active == true)'
jq '.[] | select(.score > 80)'
jq '.[] | select(.name | startswith("A"))'
jq '.[] | select(.tags | contains(["linux"]))'
jq '.[] | select(.status == "error" or .status == "warn")'
jq '.[] | select(.value != null)'
jq '.[] | select(has("email"))'
jq '.[] | select(.count >= 10 and .count <= 100)'

Output: (none — exits 0 on success)

Given [{"name":"Alice","dept":"Eng","active":true},{"name":"Bob","dept":"Ops","active":false}]:

Output:

text
{
  "name": "Alice",
  "dept": "Eng",
  "active": true
}

map and map_values

map(f) is shorthand for [.[] | f] — it applies a filter to every element of an array and collects the results. map_values(f) does the same for every value in an object, preserving the keys.

bash
jq 'map(.price * 1.1)'                  # transform each element
jq 'map(select(.active))'               # filter array
jq 'map({id, name})'                    # project fields (shorthand)
jq 'map(.tags[])'                       # flatten one level
jq '[.[] | .name] | map(ascii_upcase)'  # uppercase all names
jq 'map_values(. + 1)'                  # increment every value in object

Output: (none — exits 0 on success)

Given [{"id":1,"name":"Alice","active":true},{"id":2,"name":"Bob","active":false}]:

Output:

text
[
  {
    "id": 1,
    "name": "Alice"
  }
]

jq '[.[] | .name] | map(ascii_upcase)':

text
[
  "ALICE",
  "BOB"
]

String operations

jq provides format strings (@base64, @uri, @csv, @tsv, @html, @json, @sh) that convert a value to a specific encoding — they are most useful with -r to strip the surrounding JSON quotes. String interpolation with "\(.expr)" embeds any filter result inline.

bash
jq '.name | ascii_upcase'
jq '.name | ascii_downcase'
jq '.name | ltrimstr("prefix")'
jq '.name | rtrimstr(".json")'
jq '.name | split("/")'                 # split string → array
jq '.parts | join(", ")'                # join array → string
jq '.s | test("^Error")'               # regex test → boolean
jq '.s | match("([0-9]+)")'            # regex match object
jq '.s | gsub("foo"; "bar")'            # regex replace (jq 1.6+)
jq '"\(.name) is \(.age) years old"'   # string interpolation
jq '@base64'                            # base64 encode
jq '@base64d'                           # base64 decode
jq '@uri'                               # URL-encode
jq '@html'                             # HTML-escape
jq '@csv'                              # format as CSV row
jq '@tsv'                              # format as TSV row
jq '@json'                             # serialize to JSON string

Output: (none — exits 0 on success)

Given {"name":"Alice","age":30}:

Output:

text
"ALICE"
"alice"
"Alice is 30 years old"

jq -r '"\(.name) is \(.age) years old"':

text
Alice is 30 years old

jq -r '[.name, (.age | tostring)] | @csv':

text
"Alice",30

jq -r '[.name, (.age | tostring)] | @tsv':

text
Alice	30

Math and comparisons

jq supports standard arithmetic (+, -, *, /, %) and comparison operators (==, !=, <, >, <=, >=). Aggregation builtins — sort_by, group_by, unique_by, min_by, max_by, add — work on arrays and are typically combined with map or .[] pipelines.

bash
jq '.price | floor'
jq '.value | ceil'
jq '.x | sqrt'
jq '.a + .b'
jq 'if .score >= 90 then "A" elif .score >= 80 then "B" else "C" end'
jq '.items | sort_by(.name)'
jq '.items | sort_by(.price) | reverse'
jq '.items | unique_by(.category)'
jq '.items | group_by(.status)'
jq '.items | min_by(.price)'
jq '.items | max_by(.score)'
jq '[.[] | .price] | add'              # sum all prices
jq '[.[] | .price] | add / length'     # average price

Output: (none — exits 0 on success)

Given {"score":85}:

Output:

text
"B"

jq '[.[] | .price] | add' on [{"price":10},{"price":25},{"price":15}]:

text
50

jq '.items | sort_by(.name)' on an array of objects:

text
[
  {"name": "Alice", "dept": "Eng"},
  {"name": "Bob", "dept": "Ops"},
  {"name": "Carol", "dept": "Eng"}
]

Recursive and flatten

.. is the recursive descent operator — it produces every value at every level of nesting, making it useful for searching deeply nested documents. flatten collapses nested arrays into a single flat array; pass a depth argument to limit how many levels are collapsed.

bash
jq '.. | .name? // empty'              # recurse all levels
jq '[.. | numbers]'                    # all numbers anywhere in tree
jq 'flatten'                           # flatten nested arrays completely
jq 'flatten(1)'                        # flatten one level
jq 'paths'                             # all paths in the document
jq 'paths(scalars)'                    # paths to scalar values
jq 'getpath(["a","b"])'               # value at path
jq 'setpath(["a","b"]; 99)'            # set value at path
jq 'delpaths([["a","b"]])'            # delete at path

Output: (none — exits 0 on success)

Given [[1,[2,3]],[4,5]]:

Output:

text
[1, 2, 3, 4, 5]

jq 'flatten(1)':

text
[1, [2, 3], 4, 5]

reduce and foreach

reduce folds a stream into a single value by accumulating state across each input; foreach is similar but emits intermediate results at each step, making it useful for running totals or streaming transformations.

bash
# Sum all numbers in array
jq 'reduce .[] as $x (0; . + $x)'

# Running totals
jq '[foreach .[] as $x (0; . + $x)]'

# Group and sum
jq 'group_by(.category) | map({key: .[0].category, value: map(.amount) | add}) | from_entries'

Output: (none — exits 0 on success)

Given [10, 20, 30, 40]:

Output:

text
100

jq '[foreach .[] as $x (0; . + $x)]':

text
[10, 30, 60, 100]

jq 'group_by(.category) | map({key: .[0].category, value: map(.amount) | add}) | from_entries' on [{"category":"food","amount":12},{"category":"travel","amount":80},{"category":"food","amount":8}]:

text
{
  "food": 20,
  "travel": 80
}

Input from shell variables

Pass shell values into a jq filter without string interpolation hacks. --arg NAME VALUE injects a shell string as a jq string variable $NAME; --argjson NAME JSON parses the value as JSON first, so numbers and booleans stay typed rather than becoming strings.

bash
jq --arg name "Alice" '.[] | select(.name == $name)'
jq --argjson min 50 '.[] | select(.score > $min)'
jq -n --arg ts "$(date -Iseconds)" '{"timestamp": $ts}'

# Pass entire JSON from shell
DATA='{"key":"val"}'
echo "$DATA" | jq --argjson extra '{"extra":true}' '. + $extra'

Output:

text
{
  "name": "Alice",
  "dept": "Eng",
  "active": true
}

jq -n --arg ts "2026-04-26T14:30:00+00:00" '{"timestamp": $ts}':

text
{
  "timestamp": "2026-04-26T14:30:00+00:00"
}

echo "$DATA" | jq --argjson extra '{"extra":true}' '. + $extra':

text
{
  "key": "val",
  "extra": true
}

Null coalescing and defaults

The // operator returns the left side unless it is null or false, in which case it returns the right side. Use it to supply fallback values when fields may be missing, or chain it with empty to silently drop null outputs.

bash
jq '.missing // "default"'             # use "default" if null/false
jq '.x // empty'                       # produce no output if null/false
jq '.[] | .name // "unknown"'          # fallback per element

Output: (none — exits 0 on success)

Given {"name":null,"city":"NYC"}:

Output:

text
"default"
"unknown"
"NYC"

Practical recipes

bash
# Pretty-print and paginate
curl -s https://api.example.com/users | jq '.' | less

# Extract a flat list of values (one per line)
jq -r '.[].name' users.json

# CSV export
jq -r '.[] | [.id, .name, .email] | @csv' users.json

# Convert array of objects to key=value lines
jq -r '.[] | "\(.key)=\(.value)"' config.json

# Count items by field
jq 'group_by(.status) | map({key: .[0].status, value: length}) | from_entries' events.json

# Merge two JSON files
jq -s '.[0] * .[1]' base.json override.json

# Slurp multiple JSON lines (NDJSON) into array
jq -s '.' < ndjson.txt

# Filter NDJSON stream without slurping (memory-efficient)
jq -c 'select(.level == "error")' < ndjson.txt

# Compact all objects in array
jq -c '.[]' data.json

# Update a field in-place (print modified JSON)
jq '.version = "2.0"' package.json

# Delete a key
jq 'del(.password)' user.json

# Rename a key
jq '{new_name: .old_name} + del(.old_name)' obj.json

# Add element to array
jq '.tags += ["new-tag"]' item.json

# Get Docker container IPs
docker inspect $(docker ps -q) | jq '.[].NetworkSettings.Networks[].IPAddress' -r

# Parse GitHub API response
curl -s "https://api.github.com/repos/torvalds/linux/releases" \
  | jq -r '.[0] | "Latest: \(.tag_name) — \(.published_at)"'

# AWS CLI output processing
aws ec2 describe-instances | jq -r \
  '.Reservations[].Instances[] | "\(.InstanceId)\t\(.State.Name)\t\(.PublicIpAddress // "N/A")"'

Output: (none — exits 0 on success)

jq -r '.[].name' users.json on [{"id":1,"name":"Alice","email":"alice@example.com"},{"id":2,"name":"Bob","email":"bob@example.com"}]:

Output:

text
Alice
Bob

jq -r '.[] | [.id, .name, .email] | @csv':

text
1,"Alice","alice@example.com"
2,"Bob","bob@example.com"

jq 'group_by(.status) | map({key: .[0].status, value: length}) | from_entries' on an events array:

text
{
  "error": 3,
  "info": 12,
  "warn": 5
}

jq -c 'select(.level == "error")' on NDJSON input:

text
{"ts":"2026-01-15T10:23:45Z","level":"error","msg":"connection refused"}
{"ts":"2026-01-15T10:45:01Z","level":"error","msg":"timeout"}

jq '.version = "2.0"' package.json:

text
{
  "name": "my-app",
  "version": "2.0",
  "description": "Example app"
}

AWS CLI example output:

text
i-0a1b2c3d4e5f	running	54.210.123.45
i-1f2e3d4c5b6a	stopped	N/A

Debugging

bash
jq '. | debug'                   # print to stderr + pass through
jq '. | debug("msg: \(.key)")'   # custom debug message (jq 1.6+)
jq 'error("bad input")'          # raise an error
jq 'halt_error(1)'               # exit with code

Output:

text
["DEBUG:","msg: Alice"]
{
  "name": "Alice",
  "age": 30
}

Use jq -n (null input) to build JSON from scratch without needing an input file: jq -n '{name: "Alice", ts: now | todate}'. The now builtin returns the current Unix timestamp.

What's new in jq 1.7 / 1.8

After a five-year gap, jq 1.7.0 shipped in September 2023, followed by 1.7.1 (December 2023) and 1.8.0 (June 2024). Together they added long-requested builtins (pick, INDEX, IN, JOIN, trim, abs, toboolean, skip), fixed several CVEs, and made breaking changes to string indexing and assignment semantics. Check jq --version before relying on anything below.

Selecting fields with pick (1.7)

pick(path_expression) projects an object down to only the named paths and returns null for missing leaves. It is the long-missing inverse of del and the typed alternative to with_entries(select(...)).

bash
jq 'pick(.a, .b.c, .x)'        # {"a":1,"b":{"c":2},"x":null}
jq 'pick(.[] | .id)'           # keep only .id on every array element

Output: given {"a":1,"b":{"c":2,"d":3},"e":4}:

text
{
  "a": 1,
  "b": {
    "c": 2
  },
  "x": null
}

SQL-style INDEX, IN, JOIN (1.7)

These three builtins make join-style work over arrays of objects far more readable than hand-rolled reduce pipelines:

  • INDEX(stream; key_expr) — turn a stream of objects into a lookup table keyed by key_expr.
  • IN(stream) — boolean test, true when the input value appears anywhere in stream.
  • JOIN($idx; key_expr; join_expr) — like a SQL LEFT JOIN against an INDEX-built table.
bash
# Build a lookup table from an array
jq 'INDEX(.id)' users.json
# {"u1":{"id":"u1","name":"Alice"}, "u2":{...}}

# Filter to rows whose status is in an allowlist stream
jq '.[] | select(.status | IN("error","warn"))' events.json

# Join orders against a users index
jq -n --slurpfile orders orders.json --slurpfile users users.json '
  $orders[0] | JOIN(($users[0] | INDEX(.id)); .user_id; .[0] + {user: .[1].name})
'

Output: (none — exits 0 on success)

Trim, abs, toboolean, skip (1.8)

trim/0, ltrim/0, rtrim/0 strip whitespace from strings (no need for gsub("^\\s+|\\s+$"; "") anymore). trimstr/1 strips a specific substring from both ends. abs returns absolute value while preserving the original numeric literal (unlike fabs, which forces a float). toboolean parses the strings "true"/"false" into booleans. skip($n; f) is the dual of limit($n; f).

bash
jq '"  hello  " | trim'              # "hello"
jq '"foo.bar.foo" | trimstr("foo")'  # ".bar."
jq '-42 | abs'                       # 42
jq '"true" | toboolean'              # true
jq '[skip(2; range(5))]'             # [2,3,4]

Output: (none — exits 0 on success)

NUL-separated output for safe pipelines (1.7)

--raw-output0 emits raw strings separated by NUL bytes instead of newlines, so filenames containing newlines round-trip safely through xargs -0, cpio -0, or find -print0 pipelines.

bash
jq -r --raw-output0 '.files[]' manifest.json | xargs -0 ls -l

Output: (none — exits 0 on success)

Color controls (1.7)

NO_COLOR=1 (the no-color.org convention) and the expanded JQ_COLORS env var let you theme jq output without flags. jq 1.8 accepts truecolor (2;R;G;B) escapes inside JQ_COLORS:

bash
NO_COLOR=1 jq '.' data.json
JQ_COLORS="1;30:0;37:0;37:0;37:0;32:1;37:1;37:2;37" jq '.' data.json

Output: (none — exits 0 on success)

Breaking changes worth knowing

  • String indexing is now in code points, not bytes. indices, index, rindex, splits, [a:b] on strings count Unicode code points in 1.8 — scripts that hand-counted UTF-8 byte offsets may produce different results.
  • tonumber rejects whitespace. " 12 " | tonumber now errors; pipe through trim first.
  • Removed: leaf_paths, recurse_down, pow10, the private _nwise filter. Use paths(scalars), recurse, exp10 instead.
  • --indent 0 no longer implies compact. Use -c for compact output.
  • Array/object size cap of 2^29 (≈537M) elements (CVE-2024-23337 mitigation).

Modern alternatives

jq is the de-facto standard, but several actively-maintained tools cover adjacent use cases — faster execution, additional input formats, binary parsing, or a grep-friendly representation. Pick by data format and workflow rather than treating any of these as a drop-in replacement.

ToolLanguageStrengthWhen to reach for it
jaqRustFaster startup and execution than jq 1.7 on most benchmarks; jq-language compatibleMany small JSON files, CI pipelines, perf-critical scripts
gojqGoPure-Go jq with YAML I/O and better error messagesSingle static binary in container images; YAML pipelines
yq (Mike Farah)GoYAML-first processor; reads/writes JSON, XML, CSV, TOML, propertiesEditing Kubernetes manifests, Helm values, GitHub Actions workflows
daselGoMulti-format selector for JSON, YAML, TOML, XML, CSV with a single query languageCross-format ETL where the input/output formats differ
fqGojq language applied to binary formats (mp4, pcap, ELF, PNG, …)Inspecting binary files with jq filters
gron / fastgronGo / C++Flatten JSON to greppable path = value; lines and backDiscovering the path you need before writing the jq filter; grep-only environments
jlessRustCurses-style interactive JSON pager with collapsible nodesExploring unfamiliar JSON before reaching for a query
fxGoInteractive viewer plus full-script transformationsExploratory work and one-off ad-hoc transforms

A common pairing is curl ... | gron | grep <keyword> to find the exact path, then write the equivalent jq filter once. For YAML pipelines, yq -o=json '.' file.yaml | jq '...' lets you keep using familiar jq syntax against YAML sources.

Sources