A small jq cookbook
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 awhile readloop or another program; pretty-printed JSON is for humans,-cis for pipes.-e— set the exit code from the result: non-zero if the output isnullorfalse. That lets jq drive anifin 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.