A small jq cookbook

· shell json

jq is the tool you reach for the third time you pipe JSON into grep and regret it. It's a tiny language for slicing and reshaping JSON on the command line. You don't need the whole manual — about a dozen patterns cover most real work. Here they are, with input you can paste.

Sample data for the examples below:

{
  "users": [
    { "name": "ada",  "age": 36, "roles": ["admin", "dev"] },
    { "name": "linus","age": 54, "roles": ["dev"] }
  ]
}

Pull out a field

The dot is the identity filter; everything builds from it.

echo '{"name":"ada","age":36}' | jq '.name'      # "ada"
echo '{"name":"ada","age":36}' | jq -r '.name'   # ada   (raw, no quotes)

-r (raw output) is the flag you'll use constantly — it strips the JSON quotes so the value drops cleanly into shell variables and other tools.

Reach into nested data and arrays

jq '.users[0].name'        # "ada"   — first element
jq '.users[].name'         # every name, one per line
jq '.users | length'       # 2       — array length
jq '.users[-1]'            # last element

.users[] iterates the array and emits each element as a separate result. That "stream of values" idea is the heart of jq — most pipelines are just transforming that stream.

Filter with select

select(condition) keeps only the elements where the condition is true:

jq '.users[] | select(.age > 40)'
jq '.users[] | select(.roles | contains(["admin"]))'
jq -r '.users[] | select(.age > 40) | .name'    # linus

Read it left to right: explode the array, keep the ones that match, pull a field. This three-stage shape — iterate, filter, project — is most of what you'll write.

Reshape into a new object

{} builds a new object; map() applies a transform across an array:

jq '.users | map({ who: .name, senior: (.age > 40) })'
[
  { "who": "ada",   "senior": false },
  { "who": "linus", "senior": true }
]

This is where jq stops being a fancy grep and becomes a data-shaping tool — you pull an API response apart and emit exactly the shape your next step wants.

JSON to CSV / TSV

Turning JSON into something a spreadsheet or cut can read is a daily need:

jq -r '.users[] | [.name, .age] | @csv'
# "ada",36
# "linus",54

jq -r '.users[] | [.name, .age] | @tsv'   # tab-separated

@csv and @tsv handle quoting and escaping for you — don't hand-roll this with string concatenation.

Default values and missing keys

A missing key is null, which breaks downstream steps. // supplies a fallback:

jq -r '.users[] | .nickname // .name'    # use nickname, fall back to name
jq '.config.timeout // 30'               # default when the key is absent

Keys, values, and counting

jq 'keys'                                  # sorted key names of an object
jq '.users | group_by(.age > 40)'         # partition into buckets
jq '[.users[].roles[]] | unique'          # all distinct roles across users
jq '.users | map(.age) | add / length'    # average age

Two flags worth remembering

  • -c — compact output, one JSON object per line. This is the format for feeding a while read loop or another program; pretty-printed JSON is for humans, -c is for pipes.
  • -e — set the exit code from the result: non-zero if the output is null or false. That lets jq drive an if in a shell script:
if jq -e '.enabled' config.json >/dev/null; then
  echo "feature is on"
fi

The one habit that makes jq click

Build filters incrementally. Start with ., see the whole document, then add one stage at a time — . | .users, then .users[], then select(...), checking the output at each step. jq pipelines read left to right like a Unix pipe, and you debug them the same way: add a stage, look, add the next. Once that clicks, the manual stops being intimidating and becomes a lookup for the occasional exotic function.

Common problems

jq: error: Cannot iterate over null

You tried to .[] a key that doesn't exist, so it evaluated to null. Use the optional operator to skip missing keys quietly: .items[]?, or supply an empty array first: (.items // []) | .[].

My output has quotes around it

That's JSON string formatting. Add -r (raw output) and the surrounding quotes disappear, which is what you want when feeding the value into a shell variable or another command.

jq: error: Invalid numeric literal

jq got something that isn't valid JSON — usually log lines or a prompt mixed into the stream. Isolate just the JSON before piping it in, or if the source emits one JSON object per line, read it with jq -c in a loop rather than as one document.