$linuxjunkies
>

How to Use jq for JSON on the Command Line

Learn to filter, transform, and reshape JSON on the command line with jq — covering selectors, map/select, object construction, and real API pipelines.

IntermediateUbuntuDebianFedoraArch9 min readUpdated June 7, 2026

Before you start

  • Basic familiarity with the bash shell and pipelines
  • jq 1.6 or later (1.7 recommended for latest features)
  • curl installed if following API examples

jq is a lightweight, portable command-line JSON processor written in C. It lets you slice, filter, map, and transform JSON data with the same ease that sed and awk handle plain text. Whether you're parsing REST API responses, munging config files, or building shell pipelines, jq is the right tool. This guide covers installation, core filter syntax, practical transformations, and real-world API patterns.

Installation

Debian / Ubuntu

sudo apt update && sudo apt install jq

Fedora / RHEL 9+ / Rocky Linux

sudo dnf install jq

Arch Linux

sudo pacman -S jq

Verify the install and note the version — syntax for some features (like try-catch and $ENV) requires jq 1.6+.

jq --version

Core Concepts

jq reads JSON from stdin or a file and applies a filter — an expression that selects, reshapes, or computes a value. The simplest filter is ., which means "the whole input."

echo '{"name":"alice","age":30}' | jq .

Output (pretty-printed, with color in a terminal):

{
  "name": "alice",
  "age": 30
}

Reading from a file

jq . data.json

Compact output

Use -c to suppress pretty-printing — essential when piping into another tool.

jq -c . data.json

Raw string output

Use -r to strip JSON string quotes. Without it, a string value prints with surrounding quotes, which breaks shell variable assignments.

echo '{"name":"alice"}' | jq -r '.name'
# prints: alice  (no quotes)

Selectors and Field Access

Object field access

echo '{"user":{"id":42,"login":"alice"}}' | jq '.user.login'

Array indexing

echo '["a","b","c"]' | jq '.[1]'   # "b"
jq '.[0:2]' <<<'["a","b","c"]'   # ["a","b"]

Iterating arrays with .[]

The iterator .[] explodes an array into a stream of values, one per output line.

echo '[{"id":1},{"id":2}]' | jq '.[].id'

Optional operator .foo?

Appending ? suppresses errors when a key doesn't exist — useful in heterogeneous data.

echo '{"a":1}' | jq '.b?'   # outputs: null (no error)

Filters and Pipes

jq uses | exactly like the shell: the output of the left expression becomes the input of the right.

echo '[{"name":"alice","score":90},{"name":"bob","score":75}]' \
  | jq '.[] | .name'

select() — conditional filtering

select(expr) passes a value through only if expr is truthy. This is the jq equivalent of grep or WHERE.

echo '[{"name":"alice","score":90},{"name":"bob","score":75}]' \
  | jq '[.[] | select(.score > 80)]'

map() — transform every element

map(f) is shorthand for [.[] | f] — it applies a filter to every element and returns an array.

echo '[1,2,3,4]' | jq 'map(. * 2)'   # [2,4,6,8]

Combining select and map

cat users.json | jq '[.[] | select(.active == true) | .email]'

Building New Objects and Arrays

Object construction

Wrap { } around key-value pairs to build a new object. Keys can be literals or computed expressions in parentheses.

echo '{"first":"Alice","last":"Smith","dept":"ops"}' \
  | jq '{full_name: (.first + " " + .last), department: .dept}'

Array construction

echo '{"a":1,"b":2,"c":3}' | jq '[.a, .b]'   # [1,2]

keys, values, has

echo '{"x":10,"y":20}' | jq 'keys'     # ["x","y"]
echo '{"x":10,"y":20}' | jq 'values'   # [10,20]
echo '{"x":10}' | jq 'has("x")'        # true

Transformations and Builtins

String interpolation

echo '{"host":"db1","port":5432}' \
  | jq -r '"Connect to \(.host):\(.port)"'

Type conversion

echo '"42"' | jq 'tonumber'   # 42
echo '42'   | jq 'tostring'   # "42"

length, sort_by, group_by, unique_by

# Sort users by score descending
cat users.json | jq 'sort_by(.score) | reverse'

# Count items in an array
echo '[1,2,3]' | jq 'length'   # 3

# Unique values
echo '[1,2,2,3,1]' | jq 'unique'   # [1,2,3]

reduce — aggregations

echo '[1,2,3,4,5]' | jq 'reduce .[] as $x (0; . + $x)'   # 15

env and $ENV

jq 1.6+ can read environment variables directly — avoids shell quoting pitfalls when injecting values into filters.

TARGET_USER=alice jq -n --arg user "$TARGET_USER" '$user'

Passing Variables with --arg and --argjson

Never interpolate shell variables directly into jq filter strings — that's an injection risk. Use --arg (string) or --argjson (parsed JSON) instead.

# --arg injects a shell string as a jq string variable
jq --arg username "$USER" '.[] | select(.login == $username)' users.json

# --argjson injects a value that jq will parse as JSON
jq --argjson threshold 80 '[.[] | select(.score >= $threshold)]' scores.json

Working with Real APIs

Parsing curl output

curl -s https://api.github.com/users/torvalds | jq '{login: .login, repos: .public_repos, created: .created_at}'

Extracting a list of values for shell loops

# Get all repo names and loop over them
for repo in $(curl -s https://api.github.com/users/torvalds/repos | jq -r '.[].name'); do
  echo "Repo: $repo"
done

Handling paginated or multi-document JSON (JSON Lines)

Some APIs and log pipelines emit one JSON object per line (NDJSON). Use --slurp to read all lines into an array, or process line-by-line without it.

# Process each JSON line independently
cat events.ndjson | jq -c 'select(.type == "error") | .message'

Combining jq with xargs

# Delete a list of AWS-style resource IDs retrieved from an API
curl -s https://example.com/api/items \
  | jq -r '.items[] | select(.expired) | .id' \
  | xargs -I{} ./delete_item.sh {}

Verification

A quick sanity check: run a multi-stage filter against known data and confirm the output matches expectations.

echo '[{"name":"alice","active":true},{"name":"bob","active":false}]' \
  | jq -r '[.[] | select(.active) | .name] | join(", ")'

Expected output: alice. If you see a parse error, double-check your shell quoting — single quotes around the jq filter prevent the shell from expanding $ characters inside it.

Troubleshooting

  • null output instead of a value: The key path is wrong or the key doesn't exist. Try jq 'keys' first to inspect available fields.
  • Cannot index array with string: You're using object-style access (.name) on an array. Add .[] to iterate first.
  • Unexpected string output with quotes: Add -r for raw (unquoted) string output.
  • Filter works interactively but fails in a script: You're likely using double quotes around the filter and a $variable inside it is being expanded by bash before jq sees it. Switch to single quotes or use --arg.
  • jq: command not found in a minimal container: Alpine users: apk add jq. Many minimal Docker images omit it.
tested on:Ubuntu 24.04Fedora 40Arch rollingDebian 12

Frequently asked questions

What is the difference between --arg and --argjson?
--arg always injects the value as a JSON string. --argjson parses the value as JSON first, so you can inject numbers, booleans, arrays, or objects rather than just strings.
Why does my filter return null instead of the expected value?
The most common cause is a wrong key name or accessing an object key on an array. Run jq 'keys' on your input first to see the actual top-level structure, then trace each step of your path.
How do I process a file with one JSON object per line (NDJSON)?
By default jq handles one complete JSON value. For NDJSON/JSON Lines files, either stream them without --slurp (each line is processed independently) or add --slurp (-s) to read all lines into a single array.
Can jq modify a JSON file in-place?
jq itself has no in-place flag. The standard pattern is: jq 'filter' file.json > tmp.json && mv tmp.json file.json. Some scripts use sponge from moreutils to avoid the temp file.
How do I handle JSON that contains keys with special characters or spaces?
Use quoted key syntax: .[ "my-key" ] or .[ "key with spaces" ] instead of dot notation. This works anywhere a normal field selector would.

Related guides