Filtering and Selecting JSON with jq Pipelines
Navigate nested objects and arrays using jq selectors, pipes, and the select filter.
Filtering and Selecting JSON with jq Pipelines is a free DevOps Bootcamp lesson on CoddyKit — lesson 1 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the DevOps Bootcamp learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
What is jq and Why Use It?
jq is a lightweight, powerful command-line tool for parsing, filtering, and transforming JSON data. It is the sed of JSON — you pipe JSON into it and get structured output back.
- Pre-installed on most Linux distros or available via
apt install jq/brew install jq - Works seamlessly in shell pipelines with
curl,cat, and other tools - Supports filtering, mapping, reduction, and format conversion
The basic invocation is: jq '<filter>' file.json or piped as cat file.json | jq '<filter>'. The filter . (dot) is the identity — it pretty-prints the whole document.
# Pretty-print a JSON file
jq '.' data.json
# Or pipe from curl
curl -s https://api.github.com/users/torvalds | jq '.'Selecting Object Fields with Dot Notation
To access a field in a JSON object, use dot notation: .fieldName. You can chain selectors to navigate nested objects.
.name— top-level field.address.city— nested field."field-with-dash"— fields with special characters need quotes
If the field does not exist, jq returns null rather than erroring. This makes it safe to use in scripts without extra null-checks for optional fields.
# Given: {"name":"Alice","address":{"city":"Berlin","zip":"10115"}}
echo '{"name":"Alice","address":{"city":"Berlin","zip":"10115"}}' | jq '.name'
# Output: "Alice"
echo '{"name":"Alice","address":{"city":"Berlin","zip":"10115"}}' | jq '.address.city'
# Output: "Berlin"Accessing Array Elements and Iterating
JSON arrays are accessed with bracket notation. jq uses zero-based indexing.
.items[0]— first element.items[-1]— last element.items[1:3]— slice (index 1 up to but not including 3).items[]— explode the array: outputs each element as a separate value (this is the iterator)
The iterator [] is fundamental to jq pipelines — it lets you apply subsequent filters to every element independently.
# Given an array of users
echo '[{"name":"Alice"},{"name":"Bob"},{"name":"Carol"}]' | jq '.[0]'
# Output: {"name":"Alice"}
# Iterate all elements and extract .name from each
echo '[{"name":"Alice"},{"name":"Bob"},{"name":"Carol"}]' | jq '.[].name'
# Output:
# "Alice"
# "Bob"
# "Carol"Building jq Pipelines with the Pipe Operator
Just like the shell pipe |, jq has its own internal pipe operator. It passes the output of one filter as the input to the next.
jq '.users[] | .name'— iterate users, then extract name from eachjq '.data | .items[] | .id'— navigate to data, explode items, extract id
Pipes inside a jq expression allow complex transformations to be built step by step. Each stage receives whatever the previous stage produced — including multiple values from an iterator.
Key insight: when an iterator produces N values, every downstream filter runs N times, once per value.
# Nested pipeline: navigate -> iterate -> extract
echo '{"users":[{"name":"Alice","age":30},{"name":"Bob","age":25}]}' \
| jq '.users[] | .name'
# Output:
# "Alice"
# "Bob"
# Chain more stages
echo '{"users":[{"name":"Alice","age":30},{"name":"Bob","age":25}]}' \
| jq '.users[] | .age'
# Output:
# 30
# 25Filtering with select()
The select(condition) filter passes a value through only if the condition is true; otherwise it produces no output. It is the jq equivalent of grep or WHERE in SQL.
select(.age > 18)— keep objects where age is greater than 18select(.status == "active")— equality checkselect(.name | startswith("A"))— nested string test
Combine select with the iterator to filter arrays: .items[] | select(.active) outputs only elements where .active is truthy.
# Filter array elements by a condition
echo '[{"name":"Alice","age":30},{"name":"Bob","age":17},{"name":"Carol","age":25}]' \
| jq '.[] | select(.age >= 18) | .name'
# Output:
# "Alice"
# "Carol"
# Filter by string equality
echo '[{"name":"Alice","role":"admin"},{"name":"Bob","role":"user"}]' \
| jq '.[] | select(.role == "admin") | .name'
# Output: "Alice"Reconstructing Objects and Arrays with {} and []
jq lets you reshape data by constructing new objects with {} and new arrays with [].
{name: .name, city: .address.city}— pick and rename fields into a new object[.items[] | .id]— collect iterated values back into an array- Shorthand:
{name, age}is equivalent to{name: .name, age: .age}
Wrapping a pipeline in [...] is called array construction and is essential when you want a JSON array as output rather than a stream of values.
# Reshape: keep only selected fields
echo '[{"id":1,"name":"Alice","password":"secret"},{"id":2,"name":"Bob","password":"secret"}]' \
| jq '[.[] | {id, name}]'
# Output:
# [
# {"id": 1, "name": "Alice"},
# {"id": 2, "name": "Bob"}
# ]
# Collect filtered names into an array
echo '[{"name":"Alice","active":true},{"name":"Bob","active":false}]' \
| jq '[.[] | select(.active) | .name]'
# Output: ["Alice"]Working with Nested Arrays and Recursive Descent
Real-world JSON is often deeply nested. jq provides two tools for deep navigation:
.a.b.c— explicit path when structure is known.. | .fieldName?— recursive descent: walks every node in the tree and outputs values where the key exists
The ? (try) operator suppresses errors when a field does not exist at a given node, which is critical when using recursive descent on heterogeneous trees.
Use recursive descent sparingly on large documents — it visits every node and can be slow. Prefer explicit paths when the structure is predictable.
# Explicit deep path
echo '{"a":{"b":{"c":42}}}' | jq '.a.b.c'
# Output: 42
# Recursive descent: find all "id" values anywhere in the tree
echo '{"users":[{"id":1,"profile":{"id":99}},{"id":2}]}' \
| jq '.. | .id?'
# Output:
# 1
# 99
# 2Practical Example: Parsing curl API Responses
One of the most common jq use-cases is parsing REST API responses fetched with curl. Combining curl -s (silent) with a jq pipeline gives you clean, scriptable data extraction.
- Extract a single value:
curl -s URL | jq '.field' - Build a summary table: iterate an array, reconstruct objects with only the fields you need
- Use
-r(raw output) to strip the surrounding quotes from string values — essential when assigning to shell variables
Tip: always add -r when the jq output will be used as a shell variable or piped to another command.
#!/usr/bin/env bash
# Fetch GitHub repo info and extract specific fields
REPO="torvalds/linux"
RESPONSE=$(curl -s "https://api.github.com/repos/${REPO}")
# Extract fields
STARS=$(echo "$RESPONSE" | jq -r '.stargazers_count')
LANG=$(echo "$RESPONSE" | jq -r '.language')
DESC=$(echo "$RESPONSE" | jq -r '.description')
echo "Stars : $STARS"
echo "Lang : $LANG"
echo "Desc : $DESC"Using map() and map_values()
jq provides two convenient higher-order functions for transforming collections:
map(f)— applies filterfto every element of an array, returning a new array. Equivalent to[.[] | f].map_values(f)— appliesfto every value in an object or array, preserving the keys/indices.
These are more readable than manually wrapping pipelines in [] and are idiomatic jq style for transformations that should stay as arrays.
# map: extract a field from each element
echo '[{"name":"Alice","score":95},{"name":"Bob","score":80}]' \
| jq 'map(.name)'
# Output: ["Alice", "Bob"]
# map with select: filter + transform in one step
echo '[{"name":"Alice","score":95},{"name":"Bob","score":60}]' \
| jq 'map(select(.score >= 70) | .name)'
# Output: ["Alice"]
# map_values: multiply every value in an object by 2
echo '{"a":1,"b":2,"c":3}' | jq 'map_values(. * 2)'
# Output: {"a":2,"b":4,"c":6}Handling Optional Fields and Defaults with //
JSON data from external sources is often inconsistent — fields may be missing or null. jq provides the alternative operator // (double slash) to supply a default value.
.nickname // "anonymous"— use.nicknameif it is not null/false, otherwise use"anonymous".count // 0— numeric default- Combine with
select:select((.status // "inactive") == "active")
This is far more concise than the shell equivalent of ${VAR:-default} and composes cleanly inside longer pipelines.
# Provide defaults for missing/null fields
echo '[{"name":"Alice","role":"admin"},{"name":"Bob"}]' \
| jq '[.[] | {name, role: (.role // "user")}]'
# Output:
# [
# {"name": "Alice", "role": "admin"},
# {"name": "Bob", "role": "user"}
# ]
# Numeric default
echo '{"items":[1,2,3]}' | jq '.total // 0'
# Output: 0Practical Script: JSON Log Parser
Structured JSON logging is standard in modern systems. Here is a realistic script that reads a newline-delimited JSON log file, filters for error entries, and formats a human-readable summary.
Key patterns used:
-c(compact output) — one JSON object per line, useful for piping to shell loops--arg name value— inject a shell variable as a jq string argumentselectfor log-level filtering-rfor raw string output suitable forecho
#!/usr/bin/env bash
# Parse newline-delimited JSON logs and report ERRORs
# Each log line: {"level":"ERROR","msg":"...","ts":"2024-01-15T10:23:00Z","svc":"auth"}
LOG_FILE="/var/log/app/app.log"
LEVEL="ERROR"
echo "=== $LEVEL entries in $LOG_FILE ==="
jq -r --arg lvl "$LEVEL" \
'select(.level == $lvl) | "[\(.ts)] [\(.svc)] \(.msg)"' \
"$LOG_FILE"
# Count errors per service
echo ""
echo "=== Error count by service ==="
jq -r --arg lvl "$LEVEL" \
'select(.level == $lvl) | .svc' "$LOG_FILE" \
| sort | uniq -c | sort -rnKnowledge Check: jq select() Behaviour
Test your understanding of how select() works inside a jq pipeline.
Given the following command:
echo '[{"name":"Alice","age":30},{"name":"Bob","age":17},{"name":"Carol","age":22}]' | jq '[.[] | select(.age >= 18) | .name]'What will the output be?
Lesson Recap: jq Pipelines for JSON Filtering
You have covered the core jq toolkit for navigating and filtering JSON from the command line:
- Dot notation (
.field,.a.b.c) selects fields from objects - Array access (
.[0],.[]) indexes and iterates arrays - Pipe operator (
|) chains filters; each stage processes all values from the previous one - select(cond) filters values, passing only those where the condition is truthy
- Object/array construction (
{},[],map()) reshapes data into new structures - Alternative operator (
//) supplies defaults for null or missing fields - -r flag strips quotes for shell variable assignment; --arg injects shell variables safely
- Recursive descent (
.. | .field?) searches deeply nested trees when the path is unknown
With these building blocks you can transform any JSON API response, log file, or configuration into exactly the data your scripts need — all without leaving the terminal.
Frequently asked questions
Is the “Filtering and Selecting JSON with jq Pipelines” lesson free?
Yes — the full text of “Filtering and Selecting JSON with jq Pipelines” is free to read here on the web, and the DevOps Bootcamp course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the DevOps Bootcamp course, upgrade to CoddyKit PRO.
What will I learn in “Filtering and Selecting JSON with jq Pipelines”?
Navigate nested objects and arrays using jq selectors, pipes, and the select filter. You practise DevOps Bootcamp with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start DevOps Bootcamp?
No prior experience is required. DevOps Bootcamp on CoddyKit is structured for beginners through advanced learners; this is — lesson 1 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Filtering and Selecting JSON with jq Pipelines” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this DevOps Bootcamp lesson?
Yes. Every DevOps Bootcamp lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Filtering and Selecting JSON with jq Pipelines
- Transforming and Building JSON Objects with jq
- Consuming REST APIs with curl and jq Together
- Editing YAML Configuration Files with yq