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
[
{ "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
| Goal | Command | Output |
|---|---|---|
| Pretty-print the whole file | jq . users.json | The file, indented with 2 spaces |
| First element | jq '.[0]' users.json | {"name": "Ada", ...} |
| One field of the first element | jq '.[0].name' users.json | "Ada" |
| Last element | jq '.[-1].name' users.json | "Grace" |
| Slice | jq -c '.[0:2] | map(.name)' users.json | ["Ada","Linus"] |
| Iterate an array | jq '.[].name' users.json | "Ada" "Linus" "Grace" (three lines) |
| Count elements | jq 'length' users.json | 3 |
| Key with spaces or dashes | jq '.["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
| Flag | Effect | Example |
|---|---|---|
-r (--raw-output) | Print strings without quotes | jq -r '.[].name' prints Ada not "Ada" |
-c (--compact-output) | One line per result | jq -c '.[]' prints one object per line (NDJSON) |
-s (--slurp) | Read all input values into one array | jq -s 'length' events.ndjson counts lines |
-n (--null-input) | Do not read input; build JSON from scratch | jq -n '{ok: true}' |
-e (--exit-status) | Exit 1 if the last output is false or null | Useful in shell if tests |
--indent n / --tab | Change indentation | jq --indent 4 . |
-S (--sort-keys) | Sort object keys in output | jq -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
Linusselect(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
2map 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
| Command | Output | Note |
|---|---|---|
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
| Command | Output | Why |
|---|---|---|
jq '.[2].email' | null | Missing keys return null, no error |
jq '.[0].address.city' | null | Indexing 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.
Recursive search
.. 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.

