jq Cheatsheet

Common Recipes

Use this jq reference while you build software engineering projects, review code, or refresh the syntax you reach for most.

Reshape an API response

The most common job: take a big response and emit exactly the object you want.

jq -c '{total: (.items | length),
        names: [.items[].name],
        cheap: [.items[] | select(.price < 25) | .id]}' data.json
# {"total":3,"names":["a","b","c"],"cheap":[1,2]}

# rename and drop fields in one pass
jq -c '.items | map({id, label: .name, price})' data.json
# [{"id":1,"label":"a","price":10},...]

# add a derived field to every element
jq -c '.items | map(. + {tax: (.price * 0.1)})' data.json
# [{"id":1,...,"tax":1},{"id":2,...,"tax":2},...]

# group and total
jq -c '.items | group_by(.cat)
       | map({cat: .[0].cat, total: (map(.price) | add)})' data.json
# [{"cat":"x","total":40},{"cat":"y","total":20}]

# group into an object keyed by the grouping value
jq -c '.items | group_by(.cat)
       | map({key: .[0].cat, value: [.[].id]}) | from_entries' data.json
# {"x":[1,3],"y":[2]}

NDJSON and log lines

jq reads a stream of values, so newline-delimited JSON needs no flag at all. Given app.ndjson:

{"ts":"...:00Z","level":"info","msg":"start","dur":5}
{"ts":"...:01Z","level":"error","msg":"boom","dur":50}
{"ts":"...:02Z","level":"error","msg":"boom","dur":70}
# filter lines, emit lines
jq -c 'select(.level == "error") | {ts, msg}' app.ndjson

# tally a field without holding the file in memory
jq -nc 'reduce inputs.level as $l ({}; .[$l] += 1)' app.ndjson
# {"info":1,"error":2}

# slurp only when you need to sort or average across every line
jq -sc '[.[].dur] | add / length' app.ndjson      # 41.666666666666664
jq -sc 'group_by(.msg) | map({msg: .[0].msg, n: length})
        | sort_by(-.n) | .[0]' app.ndjson         # {"msg":"boom","n":2}

# which line was this
jq -c '{n: input_line_number, msg}' app.ndjson    # {"n":1,...} ...

# and back the other way, array to NDJSON
jq -c '.items[]' data.json

-n with inputs processes one value at a time. -s builds the entire array first, so it is the one to avoid on a large log.

Rows out, CSV, TSV, and shell loops

jq -r '.items[] | [.id, .name, .price] | @csv' data.json
# "1","a",10  ... note strings are quoted, numbers are not

# a header row, then the data
jq -r '(.items[0] | keys_unsorted), (.items[] | [.[]]) | @tsv' data.json

# ragged objects, using the union of all keys as the header
jq -r '.items | (map(keys) | add | unique) as $k
       | ($k | @csv), (.[] | [.[$k[]]] | @csv)' data.json

Feed a shell loop with @tsv, never with unquoted word splitting:

jq -r '.items[] | [.id, .name] | @tsv' data.json |
while IFS=$'\t' read -r id name; do
  echo "id=$id name=$name"
done

When a value may itself contain tabs or newlines, pass whole rows as base64:

for row in $(jq -r '.items[] | @base64' data.json); do
  echo "$row" | base64 --decode | jq -r '"\(.id):\(.name)"'
done
# 1:a  2:b  3:c

@sh quotes for the shell, which makes a small config safe to eval:

eval "$(jq -r '.meta | to_entries[] | "\(.key)=\(.value | @sh)"' data.json)"
echo "$count"        # 3

Edit a file in place

jq has no in-place flag. Redirecting to the input file truncates it before jq reads a byte, so jq '.' f.json > f.json leaves you with 0 bytes. Always write elsewhere and move:

tmp=$(mktemp)
jq '.meta.count += 1' data.json > "$tmp" && mv "$tmp" data.json

jq '.version = "9.9.9"' pkg.json > t && mv t pkg.json
jq 'del(.devDependencies)' pkg.json > t && mv t pkg.json

The && matters. If jq fails, the move never runs and the original survives.

Merge and diff two documents

jq -nc '{x:{y:1,z:2}} + {x:{y:9}}'    # {"x":{"y":9}}, shallow, right wins
jq -nc '{x:{y:1,z:2}} * {x:{y:9}}'    # {"x":{"y":9,"z":2}}, recursive

+ replaces a key outright. * descends into objects and merges leaf by leaf, which is what a config override wants.

# base.json {"port":80,"opts":{"a":1,"b":2}}
# over.json {"port":8080,"opts":{"b":9}}
jq -sc '.[0] * .[1]' base.json over.json
# {"port":8080,"opts":{"a":1,"b":9}}

# which top-level keys differ
jq -nc --slurpfile a base.json --slurpfile b over.json \
  '[($a[0] | keys[]) | select($a[0][.] != $b[0][.])]'    # ["opts","port"]

# keys only the first document has
jq -nc --slurpfile a base.json --slurpfile b over.json \
  '($a[0] | keys) - ($b[0] | keys)'

# a readable structural diff, key order normalized
diff <(jq -S . base.json) <(jq -S . over.json)

Survive ragged data

# one default covers missing and null alike
printf '{"a":1}\n{"a":null}\n{}\n' | jq -c '{a: (.a // 0)}'
# {"a":1} {"a":0} {"a":0}

# strip null-valued keys at every depth
echo '{"a":1,"b":null,"c":{"d":null,"e":2}}' |
  jq -c 'walk(if type == "object"
              then with_entries(select(.value != null)) else . end)'
# {"a":1,"c":{"e":2}}

# find a field wherever it hides, skipping every wrong-typed branch
jq -c '[.. | .price? // empty]' data.json          # [10,20,30]

# map over items that may not be an array
jq -c '[.items[]? | .id]' data.json                # [1,2,3], no error if absent

To learn the shape of a document you have never seen, print its leaves:

jq -r 'paths(scalars) as $p | "\($p | join(".")) = \(getpath($p))"' data.json
# name = api
# tags.0 = web
# items.0.id = 1  ...

Several files, and programs in a file

jq -nc '[inputs | input_filename]' data.json pkg.json
# ["data.json","pkg.json"]
jq -sc 'map(.name)' data.json pkg.json               # ["api","app"]
jq -nc 'reduce inputs as $d ({}; . + {($d.name): true})' data.json pkg.json
# {"api":true,"app":true}

Once a filter outgrows a shell line, put it in a file and drop the quoting problem entirely. Comments start with #.

printf '# how many items\n.items | length\n' > count.jq
jq -f count.jq data.json      # 3

Use jq -e when the answer is a decision rather than data. It exits 1 on false or null and 4 when nothing was output, so a shell if reads well:

if jq -e '.items | length > 0' data.json > /dev/null; then echo "has items"; fi