Linux jq Command
last modified October 23, 2026
jq is a command-line JSON processor. It reads JSON from files or
standard input, runs it through a filter, and writes the result as
JSON. Filters select fields, iterate over arrays, build new objects, compute
values, and modify documents. With jq, the output of web APIs,
docker inspect, ip -j, or a cloud CLI can be
processed in a shell pipeline as easily as plain text with grep
or awk.
The syntax is jq [options] filter [file...]. The filter is
almost always enclosed in single quotes, because it contains characters such
as |, [, and $ that the shell would
otherwise interpret. The examples use jq 1.8.1. Distributions often ship
older versions (Debian 12 and Ubuntu 22.04 have 1.6); the differences
are mentioned where they matter. When the output is a terminal,
jq prints it in colour; the transcripts here show plain text.
Table of contents
- Basics: sample data, pretty-printing, output formatting, fields, special keys, arrays, raw output
- Transforming: building objects, select, map, built-in functions, sorting and grouping, strings and regexes, CSV and TSV, format strings, nested data
- Logic: defaults and conditions, optional access
- Modifying: updating values, keys and values
- Input and variables: shell variables, null input, JSON Lines and slurp, dates
- Advanced: reduce, exit status, editing files, jq with curl, paths, functions
Sample data
Most examples work with three small files. The first is an array of user records.
[
{"id": 1, "name": "Alice Smith", "email": "alice@example.com", "age": 34, "role": "admin", "active": true,
"address": {"city": "Bratislava", "country": "SK"}, "tags": ["dev", "ops"], "created": "2024-03-15T09:30:00Z"},
{"id": 2, "name": "Bob Brown", "email": "bob@example.com", "age": 27, "role": "user", "active": false,
"address": {"city": "Prague", "country": "CZ"}, "tags": ["sales"], "created": "2025-01-02T14:05:00Z"},
{"id": 3, "name": "Carol White", "email": "carol@example.com", "age": 41, "role": "user", "active": true,
"address": {"city": "Vienna", "country": "AT"}, "tags": ["dev"], "created": "2023-11-20T08:00:00Z"},
{"id": 4, "name": "Dan Green", "email": null, "age": 19, "role": "guest", "active": true,
"address": {"city": "Bratislava", "country": "SK"}, "tags": [], "created": "2026-06-30T17:45:00Z"},
{"id": 5, "name": "Eve Black", "email": "eve@example.com", "age": 30, "role": "admin", "active": true,
"address": {"city": "Brno", "country": "CZ"}, "tags": ["ops", "security"], "created": "2022-07-08T12:00:00Z"}
]
The second file holds orders that refer to users by their ID.
{"orders": [
{"id": "A-1001", "user": 1, "status": "shipped", "items": [{"sku": "KB-01", "qty": 1, "price": 49.90}, {"sku": "MS-02", "qty": 2, "price": 19.50}]},
{"id": "A-1002", "user": 3, "status": "pending", "items": [{"sku": "MN-27", "qty": 1, "price": 229.00}]},
{"id": "A-1003", "user": 1, "status": "cancelled", "items": [{"sku": "CB-05", "qty": 3, "price": 7.25}]},
{"id": "A-1004", "user": 5, "status": "shipped", "items": [{"sku": "KB-01", "qty": 2, "price": 49.90}, {"sku": "CB-05", "qty": 1, "price": 7.25}]}
]}
The third is an application configuration.
{"app": {"name": "shop", "version": "1.4.2", "debug": true},
"server": {"host": "0.0.0.0", "port": 8080, "workers": 4},
"database": {"url": "postgres://db:5432/shop", "pool": 10, "password": "s3cret"},
"features": ["search", "cart"]}
In addition, events.jsonl contains application log events in the
JSON Lines format, one object per line; it is shown in the
slurp section.
Pretty-printing JSON
The simplest filter is ., the identity. It passes the input
through unchanged, and jq prints it formatted.
jq . config.json
The file has unusual line breaks; jq parses it and prints it
with two-space indentation, one key per line. This alone makes
jq worth having: curl -s URL | jq . turns an
unreadable one-line API response into a readable document. Invalid JSON is
reported with a line and column number.
$ cat config.json
{"app": {"name": "shop", "version": "1.4.2", "debug": true},
"server": {"host": "0.0.0.0", "port": 8080, "workers": 4},
"database": {"url": "postgres://db:5432/shop", "pool": 10, "password": "s3cret"},
"features": ["search", "cart"]}
$ jq . config.json
{
"app": {
"name": "shop",
"version": "1.4.2",
"debug": true
},
"server": {
"host": "0.0.0.0",
"port": 8080,
"workers": 4
},
"database": {
"url": "postgres://db:5432/shop",
"pool": 10,
"password": "s3cret"
},
"features": [
"search",
"cart"
]
}
Output formatting
Options control the layout of the output.
jq -c . config.json jq -S . config.json jq --indent 4 .server config.json
The -c option prints compact output, one JSON value per line,
which is ideal for further processing with line-oriented tools. The
-S option sorts object keys, which makes two documents easy to
compare with diff. The --indent option sets the
indentation width; --tab indents with tabs.
$ jq -c . config.json
{"app":{"name":"shop","version":"1.4.2","debug":true},"server":{"host":"0.0.0.0","port":8080,"workers":4},"database":{"url":"postgres://db:5432/shop","pool":10,"password":"s3cret"},"features":["search","cart"]}
$ jq -S . config.json
{
"app": {
"debug": true,
"name": "shop",
"version": "1.4.2"
},
"database": {
"password": "s3cret",
"pool": 10,
"url": "postgres://db:5432/shop"
},
"features": [
"search",
"cart"
],
"server": {
"host": "0.0.0.0",
"port": 8080,
"workers": 4
}
}
$ jq --indent 4 .server config.json
{
"host": "0.0.0.0",
"port": 8080,
"workers": 4
}
Selecting fields
A field is selected with a dot followed by its name. Dots are chained to reach nested values.
jq .app config.json jq .app.name config.json jq .server.port config.json jq .app.license config.json
Strings are printed with their quotes because the output is JSON. Numbers
and booleans are printed as they are. A field that does not exist yields
null, not an error.
$ jq .app config.json
{
"name": "shop",
"version": "1.4.2",
"debug": true
}
$ jq .app.name config.json
"shop"
$ jq .server.port config.json
8080
$ jq .app.license config.json
null
Keys with special characters
The short .name form works only for keys that look like
identifiers. Other keys must be quoted.
echo '{"first-name": "Ann", "2fa": true}' | jq '."first-name"'
echo '{"first-name": "Ann", "2fa": true}' | jq '.["2fa"]'
echo '{"first-name": "Ann", "2fa": true}' | jq '.first-name'
Both ."key" and .["key"] work. Without quotes,
.first-name is parsed as .first minus a function
called name, which does not exist, hence the compile error.
$ echo '{"first-name": "Ann", "2fa": true}' | jq '."first-name"'
"Ann"
$ echo '{"first-name": "Ann", "2fa": true}' | jq '.["2fa"]'
true
$ echo '{"first-name": "Ann", "2fa": true}' | jq '.first-name'
jq: error: name/0 is not defined at <top-level>, line 1, column 8:
.first-name
^^^^
jq: 1 compile error
Arrays
Array elements are accessed by index, slices select ranges, and
.[] iterates over all elements.
jq '.[0].name' users.json jq '.[-1].name' users.json jq -c '.[1:3] | map(.id)' users.json jq '.[].name' users.json jq '.[] | .address.city' users.json
Indexes start at zero, and negative indexes count from the end. The slice
.[1:3] contains elements 1 and 2. The .[] iterator
produces one output for each element; this is the most important concept in
jq: a filter can produce any number of results, and each of
them is passed separately to the next filter.
The pipe | inside the filter works like a shell pipe: the
output of the left side becomes the input of the right side. The filters
.[].name and .[] | .name are equivalent.
$ jq '.[0].name' users.json "Alice Smith" $ jq '.[-1].name' users.json "Eve Black" $ jq -c '.[1:3] | map(.id)' users.json [2,3] $ jq '.[].name' users.json "Alice Smith" "Bob Brown" "Carol White" "Dan Green" "Eve Black" $ jq '.[] | .address.city' users.json "Bratislava" "Prague" "Vienna" "Bratislava" "Brno"
Raw output
By default, strings are printed as JSON strings with quotes and escapes. The
-r option prints them as raw text, which is what shell scripts
need.
jq '.[0].email' users.json jq -r '.[0].email' users.json jq -r '.[].email' users.json jq -j '.[] | .name, ", "' users.json; echo
Note that a missing email prints null even in raw mode; we see
how to handle it in the defaults section. The
-j option implies -r and also omits the newline
after each output, so outputs are joined together. The comma operator
, produces the outputs of both of its sides, here a name and a
separator for each user.
$ jq '.[0].email' users.json "alice@example.com" $ jq -r '.[0].email' users.json alice@example.com $ jq -r '.[].email' users.json alice@example.com bob@example.com carol@example.com null eve@example.com $ jq -j '.[] | .name, ", "' users.json; echo Alice Smith, Bob Brown, Carol White, Dan Green, Eve Black,
Building objects and arrays
Curly braces build objects and square brackets collect outputs into an array.
jq -c '.[] | {name, city: .address.city}' users.json
jq -c '[.[] | .id]' users.json
jq -c '{total: length, names: [.[].name]}' users.json
In an object constructor, {name} is a shorthand for
{name: .name}. The brackets in [.[] | .id] collect
the five separate outputs of the iterator into one array. The last command
builds a summary object from the whole input array.
$ jq -c '.[] | {name, city: .address.city}' users.json
{"name":"Alice Smith","city":"Bratislava"}
{"name":"Bob Brown","city":"Prague"}
{"name":"Carol White","city":"Vienna"}
{"name":"Dan Green","city":"Bratislava"}
{"name":"Eve Black","city":"Brno"}
$ jq -c '[.[] | .id]' users.json
[1,2,3,4,5]
$ jq -c '{total: length, names: [.[].name]}' users.json
{"total":5,"names":["Alice Smith","Bob Brown","Carol White","Dan Green","Eve Black"]}
Filtering with select
The select(condition) function passes its input through when
the condition is true and produces nothing otherwise.
jq -c '.[] | select(.role == "admin") | {id, name}' users.json
jq -r '.[] | select(.age > 30 and .active) | .name' users.json
jq -r '.[] | select(.tags | index("ops")) | .name' users.json
jq -r '.[] | select(.email == null) | .name' users.json
Conditions use the comparison operators ==, !=,
<, >, and the like, combined with
and, or, and not. In
jq, only false and null are false; so
.active can be used directly, and index("ops"),
which returns the position or null, works as a membership test.
$ jq -c '.[] | select(.role == "admin") | {id, name}' users.json
{"id":1,"name":"Alice Smith"}
{"id":5,"name":"Eve Black"}
$ jq -r '.[] | select(.age > 30 and .active) | .name' users.json
Alice Smith
Carol White
$ jq -r '.[] | select(.tags | index("ops")) | .name' users.json
Alice Smith
Eve Black
$ jq -r '.[] | select(.email == null) | .name' users.json
Dan Green
Transforming arrays with map
The map(f) function applies a filter to each element of an
array and returns a new array. It is a shorthand for [.[] | f].
jq -c 'map(.age)' users.json
jq -c 'map(select(.active)) | map(.id)' users.json
jq -c 'map({(.name): .age}) | add' users.json
Combined with select, map filters an array while
keeping it an array, unlike .[] | select(...), which produces
separate values. In the last command, a key in parentheses is computed from
the data; add then merges the one-key objects into one object,
turning an array of records into a lookup table.
$ jq -c 'map(.age)' users.json
[34,27,41,19,30]
$ jq -c 'map(select(.active)) | map(.id)' users.json
[1,3,4,5]
$ jq -c 'map({(.name): .age}) | add' users.json
{"Alice Smith":34,"Bob Brown":27,"Carol White":41,"Dan Green":19,"Eve Black":30}
Built-in functions
jq has many built-in functions for inspecting and aggregating
data.
jq 'length' users.json
jq -c '.[0] | keys' users.json
jq '.[0] | has("phone")' users.json
jq 'map(.age) | add / length' users.json
jq -c 'map(.age) | [min, max]' users.json
jq -c '[.[].tags[]] | unique' users.json
The length function returns the number of elements of an
array, keys of an object, or characters of a string. The
keys function returns object keys sorted; keys_unsorted
keeps the original order. The average is computed as the sum divided by
the count, and unique sorts the values and removes duplicates.
$ jq 'length' users.json
5
$ jq -c '.[0] | keys' users.json
["active","address","age","created","email","id","name","role","tags"]
$ jq '.[0] | has("phone")' users.json
false
$ jq 'map(.age) | add / length' users.json
30.2
$ jq -c 'map(.age) | [min, max]' users.json
[19,41]
$ jq -c '[.[].tags[]] | unique' users.json
["dev","ops","sales","security"]
Sorting and grouping
The _by functions take an expression that computes the key for
each element.
jq -r 'sort_by(.age) | .[] | "\(.age) \(.name)"' users.json
jq -r 'sort_by(.age) | reverse | .[0].name' users.json
jq -r 'max_by(.age).name' users.json
jq -c 'group_by(.role) | map({role: .[0].role, count: length})' users.json
jq -c 'group_by(.address.country) | map({(.[0].address.country): map(.name)}) | add' users.json
The sort_by function sorts in ascending order; for the reverse
order we pipe the result to reverse. The
min_by and max_by functions return the whole
element, not just the key. The group_by function sorts the
array and splits it into arrays of elements with equal keys; we then count
the members of each group, which is the jq equivalent of SQL
GROUP BY.
$ jq -r 'sort_by(.age) | .[] | "\(.age) \(.name)"' users.json
19 Dan Green
27 Bob Brown
30 Eve Black
34 Alice Smith
41 Carol White
$ jq -r 'sort_by(.age) | reverse | .[0].name' users.json
Carol White
$ jq -r 'max_by(.age).name' users.json
Carol White
$ jq -c 'group_by(.role) | map({role: .[0].role, count: length})' users.json
[{"role":"admin","count":2},{"role":"guest","count":1},{"role":"user","count":2}]
$ jq -c 'group_by(.address.country) | map({(.[0].address.country): map(.name)}) | add' users.json
{"AT":["Carol White"],"CZ":["Bob Brown","Eve Black"],"SK":["Alice Smith","Dan Green"]}
Strings and regular expressions
String interpolation with \(expr) inserts values into a
string. Many functions work with strings, and regular expressions use the
PCRE-like Oniguruma syntax.
jq -r '.[] | "\(.id): \(.name) lives in \(.address.city)"' users.json
jq -r '.[] | .name | ascii_upcase' users.json
jq -r '.[0].name | split(" ") | .[1]' users.json
jq -r '.[0].tags | join(", ")' users.json
jq -r '.[] | select(.name | test("^[A-C]")) | .name' users.json
jq -c '.[0].email | capture("(?<user>[^@]+)@(?<domain>.+)")' users.json
jq -r '.app.version | sub("\\.\\d+$"; ".x")' config.json
The split and join functions convert between
strings and arrays. The test function checks for a match, and
capture returns named groups as an object. The
sub function replaces the first match and gsub
all matches; arguments of jq functions are separated by
semicolons. Backslashes are doubled because the regex is inside a JSON
string.
$ jq -r '.[] | "\(.id): \(.name) lives in \(.address.city)"' users.json
1: Alice Smith lives in Bratislava
2: Bob Brown lives in Prague
3: Carol White lives in Vienna
4: Dan Green lives in Bratislava
5: Eve Black lives in Brno
$ jq -r '.[] | .name | ascii_upcase' users.json
ALICE SMITH
BOB BROWN
CAROL WHITE
DAN GREEN
EVE BLACK
$ jq -r '.[0].name | split(" ") | .[1]' users.json
Smith
$ jq -r '.[0].tags | join(", ")' users.json
dev, ops
$ jq -r '.[] | select(.name | test("^[A-C]")) | .name' users.json
Alice Smith
Bob Brown
Carol White
$ jq -c '.[0].email | capture("(?<user>[^@]+)@(?<domain>.+)")' users.json
{"user":"alice","domain":"example.com"}
$ jq -r '.app.version | sub("\\.\\d+$"; ".x")' config.json
1.4.x
Converting to CSV and TSV
The @csv and @tsv formats turn an array into a
line of comma- or tab-separated values.
jq -r '.[] | [.id, .name, .address.city] | @csv' users.json jq -r '.[] | [.id, .name, .role] | @tsv' users.json jq -r '["id", "name"], (.[] | [.id, .name]) | @csv' users.json
The -r option is required, otherwise each line would be printed
as a quoted JSON string. @csv quotes strings and doubles
embedded quotes; @tsv escapes tabs and newlines. The last
command prints a header row first; the comma operator emits the header, and
then one row per user.
$ jq -r '.[] | [.id, .name, .address.city] | @csv' users.json 1,"Alice Smith","Bratislava" 2,"Bob Brown","Prague" 3,"Carol White","Vienna" 4,"Dan Green","Bratislava" 5,"Eve Black","Brno" $ jq -r '.[] | [.id, .name, .role] | @tsv' users.json 1 Alice Smith admin 2 Bob Brown user 3 Carol White user 4 Dan Green guest 5 Eve Black admin $ jq -r '["id", "name"], (.[] | [.id, .name]) | @csv' users.json "id","name" 1,"Alice Smith" 2,"Bob Brown" 3,"Carol White" 4,"Dan Green" 5,"Eve Black"
Format strings
Other @ formats encode strings for different targets. A
format followed by a string literal applies the encoding to every
interpolated value.
jq -r '.app.name | @base64' config.json
jq -r '"q=\("jq & curl")" | @uri' -n
jq -r '"<b>\(.app.name)</b>" | @html' config.json
jq -r '.features | @sh "tar czf backup.tgz \(.)"' config.json
jq -r '@json "settings: \(.server)"' config.json
@base64 encodes (and @base64d decodes),
@uri percent-encodes for URLs, and @html escapes
HTML special characters. @sh quotes values for the shell, so
generated commands are safe even when the data contains spaces or quotes.
@json inserts values as JSON text.
$ jq -r '.app.name | @base64' config.json
c2hvcA==
$ jq -r '"q=\("jq & curl")" | @uri' -n
q%3Djq%20%26%20curl
$ jq -r '"<b>\(.app.name)</b>" | @html' config.json
<b>shop</b>
$ jq -r '.features | @sh "tar czf backup.tgz \(.)"' config.json
tar czf backup.tgz 'search' 'cart'
$ jq -r '@json "settings: \(.server)"' config.json
settings: {"host":"0.0.0.0","port":8080,"workers":4}
Working with nested data
Real JSON documents nest arrays inside objects inside arrays. Variables
bound with as keep a value from an outer level available
inside the inner iteration.
jq -r '.orders[] | .id as $o | .items[] | "\($o) \(.sku) x\(.qty)"' orders.json
jq -c '.orders[] | {id, total: (.items | map(.qty * .price) | add)}' orders.json
jq '[.orders[] | select(.status == "shipped") | .items[] | .qty * .price] | add' orders.json
Inside .items[], the input is an item, so the order ID would
no longer be reachable as .id; $o holds it. The
second command computes order totals. The parentheses are needed because
the value of an object key is a single term. The third command sums the
value of all shipped items.
$ jq -r '.orders[] | .id as $o | .items[] | "\($o) \(.sku) x\(.qty)"' orders.json
A-1001 KB-01 x1
A-1001 MS-02 x2
A-1002 MN-27 x1
A-1003 CB-05 x3
A-1004 KB-01 x2
A-1004 CB-05 x1
$ jq -c '.orders[] | {id, total: (.items | map(.qty * .price) | add)}' orders.json
{"id":"A-1001","total":88.9}
{"id":"A-1002","total":229}
{"id":"A-1003","total":21.75}
{"id":"A-1004","total":107.05}
$ jq '[.orders[] | select(.status == "shipped") | .items[] | .qty * .price] | add' orders.json
195.95
Defaults and conditions
The alternative operator a // b returns a unless
it is null, false, or produces nothing; then it
returns b. Full conditions are written with
if-then-elif-else-end.
jq -r '.[] | .email // "no email"' users.json jq '.app.license // "MIT"' config.json jq -r '.[] | if .age < 21 then "\(.name): junior" elif .age < 35 then "\(.name): mid" else "\(.name): senior" end' users.json
The // operator is the usual way to supply default values for
missing fields. Because it treats false like a missing value,
.debug // true returns true even when debug is
explicitly false; for booleans, use if .debug == null
then true else .debug end. In jq 1.7 and later, the
else branch is optional and defaults to ..
$ jq -r '.[] | .email // "no email"' users.json alice@example.com bob@example.com carol@example.com no email eve@example.com $ jq '.app.license // "MIT"' config.json "MIT" $ jq -r '.[] | if .age < 21 then "\(.name): junior" elif .age < 35 then "\(.name): mid" else "\(.name): senior" end' users.json Alice Smith: mid Bob Brown: mid Carol White: senior Dan Green: junior Eve Black: mid
Optional access and recursion
Indexing a value of the wrong type is an error. The ? suffix
suppresses such errors.
echo '[{"a": 1}, "text", {"a": 2}]' | jq '.[] | .a'
echo '[{"a": 1}, "text", {"a": 2}]' | jq '.[] | .a?'
jq -c '[.. | .port? // empty]' config.json
The first command stops at the string element, so the third element is
never processed. With .a?, the error is ignored and processing
continues. The recursive descent operator .. produces every
value in the document; combined with .port? and
empty, which produces no output, it finds a key at any depth.
$ echo '[{"a": 1}, "text", {"a": 2}]' | jq '.[] | .a'
1
jq: error (at <stdin>:1): Cannot index string with string "a"
$ echo '[{"a": 1}, "text", {"a": 2}]' | jq '.[] | .a?'
1
2
$ jq -c '[.. | .port? // empty]' config.json
[8080]
Updating values
Assignment operators produce a modified copy of the input.
jq -c '.server.port = 9090 | .server' config.json jq -c '.server.workers |= . * 2 | .server' config.json jq -c '.features += ["wishlist"] | .features' config.json jq -c 'del(.database.password) | .database' config.json jq -c 'map(.age += 1) | map(.age)' users.json
The = operator sets a value. The update operator
|= computes the new value from the old one, which is
. on its right side. Arithmetic shortcuts such as
+= and *= also exist, and + on arrays
concatenates them. The del function removes a key; here it
strips a secret before the config is shared. The whole document is
modified; we pipe to .server or .database only to
keep the output short.
$ jq -c '.server.port = 9090 | .server' config.json
{"host":"0.0.0.0","port":9090,"workers":4}
$ jq -c '.server.workers |= . * 2 | .server' config.json
{"host":"0.0.0.0","port":8080,"workers":8}
$ jq -c '.features += ["wishlist"] | .features' config.json
["search","cart","wishlist"]
$ jq -c 'del(.database.password) | .database' config.json
{"url":"postgres://db:5432/shop","pool":10}
$ jq -c 'map(.age += 1) | map(.age)' users.json
[35,28,42,20,31]
Working with keys and values
The to_entries function converts an object into an array of
{key, value} pairs; from_entries converts it back,
and with_entries(f) does both around a filter.
jq -c '.server | to_entries' config.json jq -r '.server | to_entries[] | "\(.key)=\(.value)"' config.json jq -c '.server | with_entries(.key |= ascii_upcase)' config.json jq -c '.server | with_entries(select(.value | type == "number"))' config.json
The second command turns a JSON object into key=value lines,
for example for an environment file. With with_entries, we
rename keys or filter them by their values. The type function
returns the JSON type of a value as a string.
$ jq -c '.server | to_entries' config.json
[{"key":"host","value":"0.0.0.0"},{"key":"port","value":8080},{"key":"workers","value":4}]
$ jq -r '.server | to_entries[] | "\(.key)=\(.value)"' config.json
host=0.0.0.0
port=8080
workers=4
$ jq -c '.server | with_entries(.key |= ascii_upcase)' config.json
{"HOST":"0.0.0.0","PORT":8080,"WORKERS":4}
$ jq -c '.server | with_entries(select(.value | type == "number"))' config.json
{"port":8080,"workers":4}
Passing shell variables
Shell variables must never be pasted into the filter text, because quotes
or other special characters in their values would break the filter or
change its meaning. Instead, they are passed as jq variables.
jq -r --arg city Bratislava '.[] | select(.address.city == $city) | .name' users.json
jq -r --argjson min 30 '.[] | select(.age >= $min) | .name' users.json
APP_ENV=staging jq -r '"env: \($ENV.APP_ENV)"' -n
jq -n --arg user alice --argjson admin true '{user: $user, admin: $admin}'
The --arg option always creates a string. The
--argjson option parses the value as JSON, so 30
becomes a number and true a boolean; with --arg,
the comparison .age >= "30" would compare a number with a
string, and all numbers sort before strings. Environment variables are
available in the $ENV object. The last command shows the safe
way to build a JSON document from shell values.
$ jq -r --arg city Bratislava '.[] | select(.address.city == $city) | .name' users.json
Alice Smith
Dan Green
$ jq -r --argjson min 30 '.[] | select(.age >= $min) | .name' users.json
Alice Smith
Carol White
Eve Black
$ APP_ENV=staging jq -r '"env: \($ENV.APP_ENV)"' -n
env: staging
$ jq -n --arg user alice --argjson admin true '{user: $user, admin: $admin}'
{
"user": "alice",
"admin": true
}
Null input
The -n option runs the filter once with null as
input, without reading any data.
jq -n '1 + 2'
jq -n '[range(5)]' -c
jq -n -c '{name: "app", ports: [80, 443]}'
jq -n -r 'now | todate'
This turns jq into a calculator and a JSON generator. The
range function generates numbers, and now returns
the current Unix time, which todate formats as ISO 8601 in UTC.
Options may appear after the filter.
$ jq -n '1 + 2'
3
$ jq -n '[range(5)]' -c
[0,1,2,3,4]
$ jq -n -c '{name: "app", ports: [80, 443]}'
{"name":"app","ports":[80,443]}
$ jq -n -r 'now | todate'
2026-10-23T16:59:57Z
JSON Lines and slurp
Log files and streaming APIs often use JSON Lines, one JSON value per line.
jq processes such input naturally, running the filter once for
each value.
{"ts": "2026-10-23T08:00:12Z", "level": "info", "msg": "server started", "port": 8080}
{"ts": "2026-10-23T08:01:40Z", "level": "warn", "msg": "slow query", "ms": 1840}
{"ts": "2026-10-23T08:02:05Z", "level": "error", "msg": "payment failed", "order": "A-1002"}
{"ts": "2026-10-23T08:03:55Z", "level": "info", "msg": "order shipped", "order": "A-1004"}
{"ts": "2026-10-23T08:04:10Z", "level": "error", "msg": "db timeout", "ms": 5000}
The file contains five log events.
jq -c 'select(.level == "error")' events.jsonl
jq -s 'length' events.jsonl
jq -s -c 'group_by(.level) | map({(.[0].level): length}) | add' events.jsonl
jq -r '[.ts, .level, .msg] | @tsv' events.jsonl
jq -n -c '[inputs | .ms // empty]' events.jsonl
Without options, each event is filtered separately, which works well for
selecting lines. The -s (slurp) option reads all values into
one array first, which is needed for counting, sorting, and grouping across
lines. With -n, the inputs function reads the
values one by one; this aggregates without holding an extra copy of the
whole input in an array.
$ jq -c 'select(.level == "error")' events.jsonl
{"ts":"2026-10-23T08:02:05Z","level":"error","msg":"payment failed","order":"A-1002"}
{"ts":"2026-10-23T08:04:10Z","level":"error","msg":"db timeout","ms":5000}
$ jq -s 'length' events.jsonl
5
$ jq -s -c 'group_by(.level) | map({(.[0].level): length}) | add' events.jsonl
{"error":2,"info":2,"warn":1}
$ jq -r '[.ts, .level, .msg] | @tsv' events.jsonl
2026-10-23T08:00:12Z info server started
2026-10-23T08:01:40Z warn slow query
2026-10-23T08:02:05Z error payment failed
2026-10-23T08:03:55Z info order shipped
2026-10-23T08:04:10Z error db timeout
$ jq -n -c '[inputs | .ms // empty]' events.jsonl
[1840,5000]
Dates
jq converts between ISO 8601 strings, Unix timestamps, and
formatted dates.
jq -r '.[] | "\(.name): \(.created | fromdateiso8601 | strftime("%d %b %Y"))"' users.json
jq -r 'map(select(.created > "2024-01-01")) | .[].name' users.json
jq '.[0].created | fromdate' users.json
The fromdateiso8601 function, or its short alias
fromdate, parses a timestamp of the form
2024-03-15T09:30:00Z into seconds since the epoch, and
strftime formats it. Because ISO 8601 strings sort in
chronological order, plain string comparison is enough to filter by date.
$ jq -r '.[] | "\(.name): \(.created | fromdateiso8601 | strftime("%d %b %Y"))"' users.json
Alice Smith: 15 Mar 2024
Bob Brown: 02 Jan 2025
Carol White: 20 Nov 2023
Dan Green: 30 Jun 2026
Eve Black: 08 Jul 2022
$ jq -r 'map(select(.created > "2024-01-01")) | .[].name' users.json
Alice Smith
Bob Brown
Dan Green
$ jq '.[0].created | fromdate' users.json
1710495000
Aggregating with reduce
The reduce expression folds a stream of values into one
result: reduce SOURCE as $x (INIT; UPDATE).
jq 'reduce .orders[].items[] as $i (0; . + $i.qty)' orders.json
jq -c 'reduce (.orders[].items[]) as $i ({}; .[$i.sku] += $i.qty)' orders.json
jq -c '.orders | map(.status) | reduce .[] as $s ({}; .[$s] += 1)' orders.json
Inside the update expression, . is the accumulator. The first
command sums quantities. The second builds an object with the quantity per
SKU; adding a number to null gives the number, so missing keys
need no initialization. The third counts orders by status, like
sort | uniq -c in the shell.
$ jq 'reduce .orders[].items[] as $i (0; . + $i.qty)' orders.json
10
$ jq -c 'reduce (.orders[].items[]) as $i ({}; .[$i.sku] += $i.qty)' orders.json
{"KB-01":3,"MS-02":2,"MN-27":1,"CB-05":4}
$ jq -c '.orders | map(.status) | reduce .[] as $s ({}; .[$s] += 1)' orders.json
{"shipped":2,"pending":1,"cancelled":1}
Exit status
By default, jq exits with 0 whenever the input is valid. The
-e option sets the exit status from the last output, which is
useful in shell conditions.
jq '.app.debug' config.json; echo "exit: $?"
jq -e '.app.license' config.json; echo "exit: $?"
jq -e '.app.debug' config.json > /dev/null && echo "debug is on"
echo '{"a": 1' | jq .; echo "exit: $?"
With -e, the exit status is 1 when the last output is
false or null, and 0 otherwise. Errors have their
own codes: 2 for usage errors, 3 for an invalid filter, and 5 for invalid
input (4 in jq 1.6), so scripts can detect broken input even without
-e.
$ jq '.app.debug' config.json; echo "exit: $?"
true
exit: 0
$ jq -e '.app.license' config.json; echo "exit: $?"
null
exit: 1
$ jq -e '.app.debug' config.json > /dev/null && echo "debug is on"
debug is on
$ echo '{"a": 1' | jq .; echo "exit: $?"
jq: parse error: Unfinished JSON term at EOF at line 2, column 0
exit: 5
Editing a file
jq has no option to edit a file in place. The result is
written to a temporary file, which then replaces the original.
jq '.app.debug = false' config.json > config.tmp && mv config.tmp config.json jq '.app.debug = true' config.json > config.json
The && ensures that the original is replaced only when
jq succeeded. The second command shows the classic mistake:
the shell truncates config.json for the redirection before
jq starts, so jq reads an empty file and the
configuration is lost. The sponge command from moreutils
(jq ... config.json | sponge config.json) is an alternative to
the temporary file.
$ jq '.app.debug = false' config.json > config.tmp && mv config.tmp config.json
$ jq .app config.json
{
"name": "shop",
"version": "1.4.2",
"debug": false
}
$ jq '.app.debug = true' config.json > config.json
$ wc -c config.json
0 config.json
Using jq with curl
jq is the natural companion of curl for working
with REST APIs. The examples use a local httpbin service on port 8080.
curl -s localhost:8080/json | jq -r '.slideshow.slides[].title'
curl -s 'localhost:8080/get?q=jq&page=2' | jq -c .args
jq -n --arg name "O'Brien \"Dev\"" '{name: $name, tags: ["new"]}' | curl -s -H 'Content-Type: application/json' -d @- localhost:8080/post | jq -c .json
curl -s localhost:8080/status/404 -w '%{http_code}\n'
The -s option of curl hides the progress meter,
which would otherwise be mixed into the terminal output. The third command
builds a request body with jq -n --arg, which escapes the
quotes in the name correctly, and sends it with -d @-, which
reads the body from standard input. httpbin echoes the body back, and
jq extracts it. Error responses often have an empty or
non-JSON body, so check the HTTP status (for example with
curl -f or -w) before piping to jq.
$ curl -s localhost:8080/json | jq -r '.slideshow.slides[].title'
Wake up to WonderWidgets!
Overview
$ curl -s 'localhost:8080/get?q=jq&page=2' | jq -c .args
{"page":"2","q":"jq"}
$ jq -n --arg name "O'Brien \"Dev\"" '{name: $name, tags: ["new"]}' | curl -s -H 'Content-Type: application/json' -d @- localhost:8080/post | jq -c .json
{"name":"O'Brien \"Dev\"","tags":["new"]}
$ curl -s localhost:8080/status/404 -w '%{http_code}\n'
404
Paths
A path is an array of keys and indexes that leads to a value. Path functions help explore unfamiliar documents.
jq -c 'paths' config.json | head -6
jq -r 'paths(scalars) | join(".")' config.json
jq -c '[paths(scalars) as $p | {($p | map(tostring) | join(".")): getpath($p)}] | add' config.json
The paths function lists every path in the document, and
paths(scalars) only those leading to strings, numbers,
booleans, or null. Joined with dots, they show the structure
of a large document at a glance. The last command flattens the
configuration into a single-level object; getpath returns the
value at a path. Older tutorials use leaf_paths, which was
removed in jq 1.8.
$ jq -c 'paths' config.json | head -6
["app"]
["app","name"]
["app","version"]
["app","debug"]
["server"]
["server","host"]
$ jq -r 'paths(scalars) | join(".")' config.json
app.name
app.version
app.debug
server.host
server.port
server.workers
database.url
database.pool
database.password
features.0
features.1
$ jq -c '[paths(scalars) as $p | {($p | map(tostring) | join(".")): getpath($p)}] | add' config.json
{"app.name":"shop","app.version":"1.4.2","app.debug":true,"server.host":"0.0.0.0","server.port":8080,"server.workers":4,"database.url":"postgres://db:5432/shop","database.pool":10,"database.password":"s3cret","features.0":"search","features.1":"cart"}
Defining functions
Longer filters can define their own functions with def. A few
built-ins also take filters as arguments.
jq -c 'def total: map(.qty * .price) | add; .orders | map({id, total: (.items | total)})' orders.json
jq -n -c '[limit(3; range(100))], first(range(10; 20))'
jq -c 'walk(if type == "string" then ascii_upcase else . end) | .app' config.json
A function definition ends with a semicolon and is followed by the main
filter. The limit and first functions stop
generators early. The walk function applies a filter to every
value of a document recursively; here it uppercases all strings. Long
filters can be stored in a file and run with jq -f script.jq.
$ jq -c 'def total: map(.qty * .price) | add; .orders | map({id, total: (.items | total)})' orders.json
[{"id":"A-1001","total":88.9},{"id":"A-1002","total":229},{"id":"A-1003","total":21.75},{"id":"A-1004","total":107.05}]
$ jq -n -c '[limit(3; range(100))], first(range(10; 20))'
[0,1,2]
10
$ jq -c 'walk(if type == "string" then ascii_upcase else . end) | .app' config.json
{"name":"SHOP","version":"1.4.2","debug":true}
Best practices for jq
- Quote the filter: Always enclose the filter in single quotes.
- Use -r for shell use: Raw output avoids stray quotes in variables and file names.
- Pass data with --arg: Never build a filter by
concatenating shell variables; use
--argand--argjson. - Build JSON with jq: Generate request bodies with
jq -ninstead ofechoand hand-written quotes. - Do not redirect to the input file: Write to a temporary file and move it into place.
- Use -e in conditions: It makes
nullandfalseresults fail.
Source
In this article, we have used the jq command to pretty-print,
query, filter, transform, and modify JSON data, convert it to CSV, process
JSON Lines, and work with REST APIs together with curl. See
also the curl and
xh articles.
Author
List all Linux tutorials.