Vibe Engines
YouTube

Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path

jq Quick Reference

Slice, filter, and reshape JSON on the command line. Pipe curl into jq and stop eye-parsing giant payloads.

jq is a small language for JSON. A program is a pipeline of filters: each takes one JSON value in and produces zero or more values out. . is the current value, | feeds every output on its left into the filter on its right, and , emits several outputs. Most of jq is that rule plus builtins.

Wrap the program in single quotes so the shell leaves it alone, and use double quotes for strings inside it: curl -s $URL | jq '.items[] | select(.ok) | .id'. Rows show the filter exactly as typed between those quotes.

Access

Whole thing, pretty
jq .
A field
jq .name · nested: jq .user.email
Array element
jq '.[0]' · last: jq '.[-1]'
All array elements
jq '.[]' — stream each item
Safe access
jq '.a?' — no error when the input isn’t an object; a missing key is already null
.["content-type"]
keys with dashes, dots or spaces need brackets (or ."content-type")
.[2:5]
slice an array or string — end exclusive; .[-2:] is the last two
..
recursive descent, every value at every depth: [.. | .id? // empty]

Filter & transform

Pick fields
jq '{name, id}' · rename: jq '{n: .name}'
Map an array
jq 'map(.price)' · or jq '.[].price'
Filter
jq '.[] | select(.age > 30)'
Count / length
jq 'length' · jq 'map(select(.ok)) | length'
Sort / unique
jq 'sort_by(.age)' · jq 'unique'
Keys
jq 'keys' · values: jq '[.[]]'
map(select(.ok))
filter but keep an array — .[] | select(…) streams separate values instead
any(.tags[]; . == "vip")
array contains a value — use inside select(…)
IN("paid", "open")
equals one of several values: select(.status | IN("paid", "open"))
test("^err"; "i")
regex match → boolean; flags go in the second argument (i = ignore case)

Aggregate & group

add
sum numbers, join strings, concat arrays, merge objects — null for an empty array
map(.n) | add / length
average — errors on an empty array (null divided by 0), so guard it
group_by(.status)
sorts by the key, then returns an array of arrays, one per distinct key
reduce .[] as $x (0; . + $x.n)
fold: a start value, then . (the accumulator) updated once per item
unique_by(.id)
one element per distinct key; output comes back sorted by that key
min_by(.age) / max_by(.age)
the whole element with the smallest / largest key, not just the number
sort_by(.a, -.b)
multi-key sort; - flips numeric keys only — reverse the result for strings

Reshape objects

{(.k): .v}
computed key — parentheses turn the key into an expression
to_entries / from_entries
{"a":1} ↔ [{"key":"a","value":1}] — the bridge for key work
with_entries(f)
map over key/value pairs: with_entries(select(.value != null)) drops null fields
del(.password, .meta.tmp)
remove keys; del(.[] | select(.x)) removes matching array items
. + {b: 2}
shallow merge — the right side wins on clashing keys
.a * .b
deep merge of two objects; arrays are replaced, not concatenated
pick(.a, .b.c)
keep only these paths, nesting preserved (jq 1.7+)

Update & defaults

.a = 1
set a value — creates missing keys and intermediate objects
.a |= . + 1
update from the old value: on the right, . is the current .a
.n += 1
also -= *=; a missing key is null, and null + 1 is 1
.name // "anon"
default when the left is null, false or produces nothing
if .a then 1 else 2 end
elif chains; else is optional in jq 1.7+ (the input passes through)
try tonumber catch "bad"
handle an error yourself; a postfix ? just drops it

Arguments & variables

--arg name val
binds $name — always a string
--argjson n 5
binds $n parsed as JSON: numbers, booleans, arrays, objects
--slurpfile cfg f.json
$cfg = an array of every value in the file
--rawfile t notes.txt
$t = the whole file as one string
$ENV.HOME
environment variables, no flag needed (also env.HOME)
. as $root | …
bind any value to reuse further down the pipeline

Strings & formats

split(",") / join(",")
string ↔ array; join turns null into an empty string
sub(re; s) / gsub(re; s)
replace the first / every regex match: gsub("\\s+"; " ")
capture("(?<u>.+)@(?<h>.+)")
named regex groups → an object {"u": …, "h": …}
ltrimstr("v") / rtrimstr
drop a fixed prefix / suffix if present; startswith / endswith test for it
tostring / tonumber
convert; tonumber errors on non-numeric strings, so wrap it in try
fromjson / tojson
parse JSON embedded in a string field / serialize a value to a string
@base64 @base64d @uri @sh
encode or decode a string; @sh quotes it safely for a shell command
todate / fromdate
epoch seconds ↔ ISO 8601; fromdate rejects fractional seconds

Output & input flags

Raw strings (no quotes)
jq -r .name — pipe into shell safely
To CSV/TSV
jq -r '.[] | [.id, .name] | @csv'
Compact
jq -c . — one line, good for streaming
Build a string
jq -r '"\(.name): \(.age)"'
-s
slurp: read every input into one array — NDJSON to an array
-n
null input: ignore stdin, or read it lazily with input / inputs
-R / -Rs
each line as a raw string / the whole input as one string
-e
exit 1 if the last output is false or null, 4 if none — for shell if
-S / --tab / --indent 4
sort keys / indent with tabs / indent with n spaces
-f prog.jq
read the filter from a file — no shell quoting, # comments allowed

Gotchas

Cannot index array with "x"
you used .x on an array — use .[].x or map(.x)
--arg and numbers
.age > $n is silently false for a string — use --argjson or ($n | tonumber)
false // "x"
gives "x": // replaces false as well as null
jq … f > f
the shell truncates f before jq reads it — write a temp file, then mv
.a.b?
? covers only its own step — .a?.b? or try for the whole path
Big integer IDs
arithmetic converts to doubles, so IDs past 2^53 lose digits; jq 1.7 keeps untouched literals exact

Recipes

Turn an API response into CSV with a header row

curl -s https://api.example.com/users \
  | jq -r '(["id","name","email"] | @csv), (.[] | [.id, .name, .email] | @csv)' \
  > users.csv

The comma runs both expressions, so the header prints first. @csv needs an array of scalars: a nested array or object in a cell is an error, so join(";") it first.

Count records per status, two ways

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

# one object in one pass: {"paid": 2, "open": 1}
jq 'reduce .[] as $o ({}; .[$o.status] += 1)' orders.json

group_by sorts first, so its output is ordered by status. The reduce version relies on null + 1 = 1 for each key’s first sighting.

Filter on values that come from the shell

min_age=18
jq --arg owner "$USER" --argjson min "$min_age" \
  '[.[] | select(.owner == $owner and .age >= $min) | .name]' people.json

Never paste a shell variable into the filter text: a quote in the value breaks the program. --arg always binds a string, so the number goes in with --argjson — with --arg the comparison silently matches nothing.

Edit a JSON file in place

jq '.version = "2.1.0" | .keywords += ["cli"] | del(.devDependencies.unused)' \
  package.json > package.json.tmp && mv package.json.tmp package.json

jq has no in-place flag, and jq … f > f empties the file before jq reads it. Write to a temp file and rename it (or pipe into sponge f from moreutils).

Convert between NDJSON and arrays, and merge files

jq -c '.[]' events.json > events.ndjson      # array -> one object per line
jq -s '.' events.ndjson > events.json         # lines -> one array
jq -s 'add' page1.json page2.json             # concatenate two arrays
jq -s '.[0] * .[1]' base.json override.json   # deep-merge two config objects

-s loads every input into memory before the filter runs. For multi-gigabyte input use -n with reduce inputs as $x (…), which reads one value at a time.

Questions about jq Cheat Sheet

How do I pass a shell variable into a jq filter?

Use --arg name "$VAR", which binds $name inside the filter as a string, or --argjson name value when the value is a number, boolean, array or object. Do not paste the variable into the quoted program text: a value containing a quote breaks the program, and untrusted input could inject filter code. Because --arg always produces a string, numeric comparisons against it fail silently, since jq orders every number before every string. Environment variables need no flag at all: read them as $ENV.NAME or env.NAME.

How do I convert JSON to CSV with jq?

Produce one array of scalar values per row and format it with @csv, using -r so the result is raw text rather than a quoted JSON string: jq -r '.[] | [.id, .name] | @csv' users.json. For a header, emit an array of column names first and join the two expressions with a comma. @csv quotes strings and doubles any embedded quotes; @tsv separates with tabs and escapes tabs, newlines and backslashes instead. Every cell must be a string, number, boolean or null, so join or tostring any nested array or object before formatting.

Can jq edit a JSON file in place?

No. jq always writes its result to standard output and has no in-place flag like sed -i. Redirecting the output back onto the input file is worse than useless: the shell truncates the file to zero bytes before jq even starts reading it. Instead, write to a temporary file and rename it over the original with mv, which is atomic on the same filesystem, so nothing ever sees a half-written file. The sponge command from moreutils does the same job inside a pipeline, because it reads all of its input before opening the output file.

What is the difference between map(f) and .[] | f in jq?

map(f) is defined as [.[] | f]: it runs f on every element and collects the results back into a single array. Writing .[] | f instead emits each result as a separate output, which is what you want when feeding lines to a shell loop with -r. The difference shows up most with select: map(select(.ok)) returns one filtered array, while .[] | select(.ok) streams the matching items one by one. On an object, map(f) still returns an array of the values; use map_values(f) to keep the keys.