Skip to content
Tools

jq Cheat Sheet: select, map, group_by, @csv and More, With Examples

· 6 min read

jq is a command-line processor for JSON: jq '<filter>' file.json reads the file, applies the filter and prints the result. The essentials are .key to read a field, .[] to iterate an array, | to chain steps, select(cond) to filter and map(f) to transform every element; add -r for raw text output.

Every example below runs against the same small file, and the output shown is what jq 1.7 prints. You can try them without installing anything: the query bar in our online JSON formatter runs jq filters locally in your browser. If you only need to pick values out of a document rather than reshape it, a JSONPath expression tester may be simpler; jq vs JSONPath explains when each fits.

Sample input: users.json

Note: Linus has email set to null, Grace has no email key at all.
[
  { "name": "Ada",   "age": 36, "team": "core",  "email": "ada@example.com" },
  { "name": "Linus", "age": 28, "team": "infra", "email": null },
  { "name": "Grace", "age": 45, "team": "core" }
]

Basic filters

GoalCommandOutput
Pretty-print the whole filejq . users.jsonThe file, indented with 2 spaces
First elementjq '.[0]' users.json{"name": "Ada", ...}
One field of the first elementjq '.[0].name' users.json"Ada"
Last elementjq '.[-1].name' users.json"Grace"
Slicejq -c '.[0:2] | map(.name)' users.json["Ada","Linus"]
Iterate an arrayjq '.[].name' users.json"Ada" "Linus" "Grace" (three lines)
Count elementsjq 'length' users.json3
Key with spaces or dashesjq '.["first-name"]' or ."first-name"Value of that key

.[] produces a stream of separate values, not an array. Wrap it in brackets, [.[].name], to collect the stream back into one array.

Output flags: -r, -c, -s, -n

FlagEffectExample
-r (--raw-output)Print strings without quotesjq -r '.[].name' prints Ada not "Ada"
-c (--compact-output)One line per resultjq -c '.[]' prints one object per line (NDJSON)
-s (--slurp)Read all input values into one arrayjq -s 'length' events.ndjson counts lines
-n (--null-input)Do not read input; build JSON from scratchjq -n '{ok: true}'
-e (--exit-status)Exit 1 if the last output is false or nullUseful in shell if tests
--indent n / --tabChange indentationjq --indent 4 .
-S (--sort-keys)Sort object keys in outputjq -S . config.json

-s is the bridge between line-delimited files and normal jq: it turns many values into one array so map, group_by and sort_by work on them. More on that format in what NDJSON is and how to process it.

select: filter by condition

$ jq -r '.[] | select(.age > 30) | .name' users.json
Ada
Grace

$ jq -c '.[] | select(.team == "core" and .age < 40)' users.json
{"name":"Ada","age":36,"team":"core","email":"ada@example.com"}

$ jq -r '.[] | select(.name | startswith("G")) | .name' users.json
Grace

$ jq -r '.[] | select(.name | test("^l"; "i")) | .name' users.json
Linus

select(cond) passes its input through when the condition is truthy and outputs nothing otherwise. Only false and null are falsy, so select(.email) keeps Ada only: Linus's null and Grace's missing key both drop out. To keep an array instead of a stream, use map(select(...)):

$ jq -c 'map(select(.team == "core")) | map(.name)' users.json
["Ada","Grace"]

$ jq 'map(select(.team == "core")) | length' users.json
2

map and building new objects

$ jq -c 'map(.name)' users.json
["Ada","Linus","Grace"]

$ jq -c 'map({name, age})' users.json
[{"name":"Ada","age":36},{"name":"Linus","age":28},{"name":"Grace","age":45}]

$ jq -c 'map({user: .name, senior: (.age >= 40)})' users.json
[{"user":"Ada","senior":false},{"user":"Linus","senior":false},{"user":"Grace","senior":true}]

$ jq 'map(.age) | add' users.json
109

$ jq 'map(.age) | add / length' users.json
36.333333333333336

{name, age} is shorthand for {name: .name, age: .age}. Wrap any computed value in parentheses. map(f) is exactly [.[] | f].

keys, has, del and to_entries

CommandOutputNote
jq -c '.[0] | keys'["age","email","name","team"]Sorted alphabetically
jq -c '.[0] | keys_unsorted'["name","age","team","email"]Original order
jq -c 'map(has("email"))'[true,true,false]Linus has the key, set to null
jq -c '.[0] | del(.email)'{"name":"Ada","age":36,"team":"core"}Remove a key
jq -c '.[2] | to_entries'[{"key":"name","value":"Grace"},{"key":"age","value":45},{"key":"team","value":"core"}]Object to key/value pairs
jq -c '.[1] | with_entries(select(.value != null))'{"name":"Linus","age":28,"team":"infra"}Drop null fields
jq -c '.[0] | with_entries(.key |= ascii_upcase)'{"NAME":"Ada","AGE":36,...}Rename every key

to_entries, from_entries and with_entries(f) are how you filter or rename keys when you do not know them in advance. with_entries(f) is shorthand for to_entries | map(f) | from_entries.

sort_by, group_by, unique_by

$ jq -c 'sort_by(.age) | map(.name)' users.json
["Linus","Ada","Grace"]

$ jq -c 'sort_by(.age) | reverse | map(.name)' users.json
["Grace","Ada","Linus"]

$ jq -c 'group_by(.team) | map({team: .[0].team, count: length})' users.json
[{"team":"core","count":2},{"team":"infra","count":1}]

$ jq -c 'map(.team) | unique' users.json
["core","infra"]

$ jq -c 'max_by(.age) | .name' users.json
"Grace"

group_by(f) returns an array of arrays, sorted by the grouping value; the map({team: .[0].team, count: length}) step turns each group into a summary row. Sort by several fields with sort_by(.team, .age).

@csv and @tsv: JSON to spreadsheet rows

$ jq -r '.[] | [.name, .age, .email] | @csv' users.json
"Ada",36,"ada@example.com"
"Linus",28,
"Grace",45,

$ jq -r '["name","age"], (.[] | [.name, .age]) | @tsv' users.json
name	age
Ada	36
Linus	28
Grace	45

@csv and @tsv take an array per row, so build the row first with [...]. Always add -r, or every line is wrapped in an extra JSON string with escaped quotes. Null becomes an empty cell. Both fail on nested objects or arrays inside a row; flatten them first, or use a JSON to CSV converter that flattens nested keys into columns for you.

Variables: --arg and --argjson

$ jq -r --arg team core '.[] | select(.team == $team) | .name' users.json
Ada
Grace

$ jq -r --argjson min 30 '.[] | select(.age > $min) | .name' users.json
Ada
Grace

$ jq -r '.[] | .name as $n | "\($n) is \(.age)"' users.json
Ada is 36
Linus is 28
Grace is 45

--arg always passes a string; comparing .age > $min with a string would be wrong, which is why numbers go through --argjson. Passing shell values this way is safer than pasting $VAR into the filter, which breaks on quotes. \(expr) interpolates inside a string.

Null-safe access and defaults

CommandOutputWhy
jq '.[2].email'nullMissing keys return null, no error
jq '.[0].address.city'nullIndexing null returns null
jq '.[0].age.years'Error: Cannot index number with "years"Indexing a number or string fails
jq '.[0].age.years?'(no output)? suppresses the error
jq -r '.[] | .email // "none"'ada@example.com none none// supplies a default for null or false
jq -c '[.[] | .email | values]'["ada@example.com"]values drops nulls

// treats false as missing too: false // true is true. If a field can legitimately be false, test with if .x == null then ... else .x end instead.

Editing values in place

$ jq -c 'map(.age += 1) | map(.age)' users.json
[37,29,46]

$ jq -c '.[1].email = "linus@example.com" | .[1]' users.json
{"name":"Linus","age":28,"team":"infra","email":"linus@example.com"}

$ jq -c 'map(.name |= ascii_downcase) | map(.name)' users.json
["ada","linus","grace"]

jq never edits the file itself. Write to a temporary file and move it: jq '...' in.json > tmp.json && mv tmp.json in.json. Redirecting straight back to in.json empties it before jq reads it.

.. walks every value at every depth. jq '[.. | .email? // empty]' collects every email anywhere in a document, and jq 'paths(type == "number")' lists the path to every number. For the equivalent selection syntax in apps and API tests, the JSONPath cheat sheet covers $..email and friends.

FAQ

How do I use jq select to filter an array?

Iterate the array and keep matching items: jq '.[] | select(.age > 30)' outputs each match separately. To get an array back, use jq 'map(select(.age > 30))'.

How do I get values without quotes in jq?

Add the -r (raw output) flag: jq -r '.[].name' prints Ada instead of "Ada". It only affects strings; objects and arrays are still printed as JSON.

How do I convert JSON to CSV with jq?

Build an array per row and pipe it to @csv with raw output: jq -r '.[] | [.name, .age] | @csv' file.json > out.csv. Nested objects must be flattened first.

What is the difference between --arg and --argjson?

--arg passes the value as a JSON string. --argjson parses the value as JSON, so numbers, booleans, arrays and objects keep their type.

How do I avoid errors when a field might be missing in jq?

Missing keys already return null. Use .field // "default" to substitute a value, append ? to suppress type errors such as .count.value?, and pipe through values or select(. != null) to drop nulls.

More JSON guides