jq is a lightweight and flexible command-line JSON processor that lets you slice, filter, map, and transform structured data with the same ease that sed, awk, and grep let you work with text.
Let’s say we have a simple JSON file named data.json:
{
"users": [
{
"id": 1,
"name": "Alice",
"email": "alice@example.com",
"active": true,
"roles": ["admin", "editor"]
},
{
"id": 2,
"name": "Bob",
"email": "bob@example.com",
"active": false,
"roles": ["viewer"]
},
{
"id": 3,
"name": "Charlie",
"email": "charlie@example.com",
"active": true,
"roles": ["editor", "viewer"]
}
],
"settings": {
"theme": "dark",
"notifications": {
"email": true,
"sms": false
}
}
}
To get the entire content of the JSON file, you’d simply use the identity filter .:
jq '.' data.json
This will output the JSON content exactly as it is, which is useful for reformatting or validating JSON.
To extract a specific field, like the theme from the settings object, you use dot notation:
jq '.settings.theme' data.json
Output:
"dark"
Notice that jq preserves the JSON string quoting. To get the raw string value without quotes, use the -r (raw output) flag:
jq -r '.settings.theme' data.json
Output:
dark
To access elements within an array, you use square brackets with the index. For example, to get the name of the first user:
jq -r '.users[0].name' data.json
Output:
Alice
You can iterate over arrays using []. This will output each element of the users array on a new line.
jq '.users[]' data.json
Output:
{
"id": 1,
"name": "Alice",
"email": "alice@example.com",
"active": true,
"roles": [
"admin",
"editor"
]
}
{
"id": 2,
"name": "Bob",
"email": "bob@example.com",
"active": false,
"roles": [
"viewer"
]
}
{
"id": 3,
"name": "Charlie",
"email": "charlie@example.com",
"active": true,
"roles": [
"editor",
"viewer"
]
}
Combining iteration with field extraction is very common. To get the names of all users:
jq -r '.users[].name' data.json
Output:
Alice
Bob
Charlie
You can also filter based on conditions. Let’s find all active users:
jq '.users[] | select(.active == true)' data.json
Output:
{
"id": 1,
"name": "Alice",
"email": "alice@example.com",
"active": true,
"roles": [
"admin",
"editor"
]
}
{
"id": 3,
"name": "Charlie",
"email": "charlie@example.com",
"active": true,
"roles": [
"editor",
"viewer"
]
}
The pipe | in jq works similarly to shell pipes, passing the output of one filter to the next. select() is a built-in function that passes through items for which the given expression evaluates to true.
To get the names of active users:
jq -r '.users[] | select(.active == true) | .name' data.json
Output:
Alice
Charlie
You can also filter for users who have a specific role, like "editor". For this, you’ll need to check if the role exists within the roles array. The contains() function is useful here, but it checks for an exact array match. A more common approach is to iterate through the roles array and check for equality.
jq -r '.users[] | select(.roles[] | . == "editor") | .name' data.json
Output:
Alice
Charlie
Here, .roles[] iterates through the roles array for each user, and select(. == "editor") checks if any of those roles match "editor".
jq also allows you to construct new JSON objects. To create a new JSON object containing just the names and emails of active users:
jq '[.users[] | select(.active == true) | {name: .name, email: .email}]' data.json
Output:
[
{
"name": "Alice",
"email": "alice@example.com"
},
{
"name": "Charlie",
"email": "charlie@example.com"
}
]
The outer [...] creates a new array, and inside, we use {key: value} syntax to construct the objects. We’re mapping the original .name and .email fields to new keys.
When you need to perform complex transformations or aggregations, jq’s ability to define and use variables with as and let becomes powerful. For example, to count how many users have the "admin" role:
jq '[.users[] | select(.roles[] == "admin")] | length' data.json
Output:
1
This first creates an array of users with the "admin" role and then gets the length of that array.
One of the most counterintuitive aspects of jq is how it handles multiple outputs from a single filter. When a filter produces more than one result (e.g., iterating over an array with .users[]), each result is emitted independently. This is why enclosing a filter that produces multiple items in [...] is essential if you want to collect those items into a single array. Without [...], these independent outputs would be streamed, potentially confusing if you expect a single JSON object as output.
The next frontier is often combining jq with other tools for more complex scripting, or exploring its advanced features like map, reduce, and custom functions.