ZetCode

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

Sample data

Most examples work with three small files. The first is an array of user records.

users.json
[
  {"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.json
{"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.

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"]}

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
&lt;b&gt;shop&lt;/b&gt;
$ 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.

events.jsonl
{"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

Source

jq manual

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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Linux tutorials.