0Pricing
DevOps Bootcamp · Lesson

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 each
  • jq '.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
# 25

Filtering 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 18
  • select(.status == "active") — equality check
  • select(.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
# 2

Practical 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 filter f to every element of an array, returning a new array. Equivalent to [.[] | f].
  • map_values(f) — applies f to 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 .nickname if 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: 0

Practical 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 argument
  • select for log-level filtering
  • -r for raw string output suitable for echo
#!/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 -rn

Knowledge 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

  1. Filtering and Selecting JSON with jq Pipelines
  2. Transforming and Building JSON Objects with jq
  3. Consuming REST APIs with curl and jq Together
  4. Editing YAML Configuration Files with yq
← Back to DevOps Bootcamp