jq JSON Command Line Filter: select, map, to_entries & Scripts
Last updated:
jq is a lightweight command-line JSON processor — echo '{"name":"Alice"}' | jq '.name' extracts the value "Alice", and curl -s api/data | jq streams all IDs from an array without writing a single line of Python or Node.js. jq filters are composable: .foo | .bar is equivalent to .foo.bar; | pipes output of one filter to the next. The select(condition) filter keeps only elements where the condition is truthy — {".users[] | select(.age > 30) | .name"} returns names of users older than 30. This guide covers basic field selection, array iteration with .[], select() for filtering, map() for transformation, to_entries/from_entries for key-value manipulation, reduce() for aggregation, and using jq in shell scripts and GitHub Actions.
Basic Filters: Field Selection and Array Iteration
The simplest jq filter is . (identity) — it passes the input through unchanged, which is enough to pretty-print any JSON. Field selection uses dot notation: .name extracts the name field, .address.city drills into nested objects. The .[] filter iterates over every element of an array (or every value of an object), emitting each as a separate output. Index notation .[0], .[2], .[-1] accesses specific elements. Slice notation .[2:5] returns elements at index 2, 3, and 4.
# Identity filter — pretty-print any JSON
echo '{"b":2,"a":1}' | jq '.'
# {
# "a": 1,
# "b": 2
# }
# Field selection — extract a top-level key
echo '{"name":"Alice","age":30}' | jq '.name'
# "Alice"
# -r flag: raw output without surrounding quotes
echo '{"name":"Alice"}' | jq -r '.name'
# Alice
# Nested field selection
echo '{"user":{"id":1,"address":{"city":"London"}}}' | jq '.user.address.city'
# "London"
# Array element access
echo '[10,20,30,40,50]' | jq '.[0]' # 10
echo '[10,20,30,40,50]' | jq '.[-1]' # 50 (last element)
echo '[10,20,30,40,50]' | jq '.[1:3]' # [20,30] (slice)
# .[] iterates — each element emitted separately
echo '[1,2,3]' | jq '.[]'
# 1
# 2
# 3
# Wrap in [] to collect iteration results back into an array
echo '[1,2,3]' | jq '[.[]]'
# [1,2,3]
# Iterate over object values
echo '{"a":1,"b":2,"c":3}' | jq '.[]'
# 1
# 2
# 3
# Optional operator ? — suppress errors for missing fields or wrong types
echo '{"name":"Alice"}' | jq '.age?' # null (no error)
echo '"not an object"' | jq '.foo?' # no output, no error
echo '[1,2,3]' | jq '.foo?' # no output, no error
# Object construction — reshape output
echo '{"id":1,"name":"Alice","secret":"xyz"}' | jq '{id: .id, name: .name}'
# {"id":1,"name":"Alice"}
# Multiple outputs with comma
echo '{"a":1,"b":2}' | jq '.a, .b'
# 1
# 2
# length builtin — works on strings, arrays, objects, null
echo '"hello"' | jq 'length' # 5
echo '[1,2,3]' | jq 'length' # 3
echo '{"a":1,"b":2}'| jq 'length' # 2
echo 'null' | jq 'length' # 0The optional operator ? is critical for robust pipelines — without it, accessing a field on a non-object (like a string or number) raises an error and stops processing. Add ? to any field access or iterator that might encounter unexpected types: .users[]? silently skips non-array values. Use jq -e in shell scripts to set a non-zero exit code when the output is false or null, enabling if jq -e '.active' config.json; then ...; fi conditional logic.
Pipes and Identity: Composing Filters
The pipe operator | is the foundation of jq — it passes the output of the left filter as input to the right filter, enabling complex transformations from simple building blocks. .foo | .bar is identical to .foo.bar but becomes essential when the intermediate value needs further processing. The comma operator , runs two filters on the same input and emits both outputs in sequence. Parentheses group sub-expressions.
# Pipe: output of left becomes input to right
echo '{"user":{"name":"Alice","score":95}}' | jq '.user | .name'
# "Alice"
# Equivalent without pipe (dot notation)
echo '{"user":{"name":"Alice","score":95}}' | jq '.user.name'
# "Alice"
# Multi-step pipeline: iterate, then extract a field
echo '[{"name":"Alice"},{"name":"Bob"}]' | jq '.[] | .name'
# "Alice"
# "Bob"
# Comma: two filters on the same input
echo '{"a":1,"b":2}' | jq '.a, .b'
# 1
# 2
# String interpolation with \(expr) — builds strings from filter output
echo '{"name":"Alice","age":30}' | jq '"Hello, \(.name)! You are \(.age) years old."'
# "Hello, Alice! You are 30 years old."
# -r with string interpolation — useful for shell output
echo '{"host":"db.example.com","port":5432}' | jq -r '"postgresql://\(.host):\(.port)/mydb"'
# postgresql://db.example.com:5432/mydb
# Arithmetic in pipes
echo '{"price":19.99,"qty":3}' | jq '.price * .qty'
# 59.97
# Alternative operator // — default values for null/false
echo '{"name":"Alice"}' | jq '.nickname // "no nickname"'
# "no nickname"
echo '{"name":"Alice","nickname":null}' | jq '.nickname // "anonymous"'
# "anonymous"
# try-catch — handle errors in a pipeline
echo '["not-a-number","42","hello"]' | jq '[.[] | try tonumber catch null]'
# [null,42,null]
# Multiple pipes building complex transformation
curl -s 'https://api.github.com/repos/stedolan/jq/releases' | jq -r '
.[] |
select(.prerelease == false) |
"v\(.tag_name) released \(.published_at[:10])"
'String interpolation "\(.expr)" is one of jq's most practical features for shell scripting — it lets you build connection strings, log messages, and file paths from JSON fields without shell variable juggling. The alternative operator // differs from a conditional: it triggers on null or false, not on missing keys (missing keys also produce null, so the behavior is effectively the same for field access). Use try-catch to handle type errors gracefully in pipelines that process heterogeneous data.
select(): Filtering Arrays and Objects
select(condition) passes its input through unchanged if the condition is truthy, and produces no output if the condition is false or null — effectively filtering a stream. When combined with .[] and wrapped in [ ], it filters arrays. The condition can be any jq expression: comparisons (==, !=, >, <=), boolean operators (and, or, not), and builtin functions like startswith(), contains(), test() (regex matching), and has().
# Basic select — filter array elements
echo '[{"name":"Alice","age":35},{"name":"Bob","age":25},{"name":"Carol","age":40}]' \
| jq '[.[] | select(.age > 30)]'
# [{"name":"Alice","age":35},{"name":"Carol","age":40}]
# select with equality
echo '[{"status":"active"},{"status":"inactive"},{"status":"active"}]' \
| jq '[.[] | select(.status == "active")]'
# select with multiple conditions (and)
echo '[{"age":35,"active":true},{"age":25,"active":true},{"age":35,"active":false}]' \
| jq '[.[] | select(.age > 30 and .active == true)]'
# [{"age":35,"active":true}]
# select with or
echo '[{"role":"admin"},{"role":"editor"},{"role":"viewer"}]' \
| jq '[.[] | select(.role == "admin" or .role == "editor")]'
# select with regex test()
echo '[{"name":"Alice"},{"name":"Bob"},{"name":"Alicia"}]' \
| jq '[.[] | select(.name | test("^Al"))]'
# [{"name":"Alice"},{"name":"Alicia"}]
# select with startswith / endswith
echo '["apple","apricot","banana","avocado"]' \
| jq '[.[] | select(startswith("a"))]'
# ["apple","apricot","avocado"]
# select with contains — check if array field contains a value
echo '[{"tags":["admin","user"]},{"tags":["user"]},{"tags":["admin","editor"]}]' \
| jq '[.[] | select(.tags | contains(["admin"]))]'
# select with has — check if key exists
echo '[{"name":"Alice","email":"a@example.com"},{"name":"Bob"}]' \
| jq '[.[] | select(has("email"))]'
# [{"name":"Alice","email":"a@example.com"}]
# select to extract specific fields after filtering
echo '[{"id":1,"name":"Alice","age":35},{"id":2,"name":"Bob","age":25}]' \
| jq '[.[] | select(.age > 30) | {id, name}]'
# [{"id":1,"name":"Alice"}]
# Filtering objects (not arrays) — select values
echo '{"alice":{"score":95},"bob":{"score":72},"carol":{"score":88}}' \
| jq 'with_entries(select(.value.score >= 85))'
# {"alice":{"score":95},"carol":{"score":88}}The test(regex) builtin uses PCRE-compatible regular expressions, making it powerful for filtering string fields — select(.email | test("@example\\.com$")) matches all example.com email addresses. When filtering objects (rather than arrays), with_entries(select(...)) is cleaner than converting to/from arrays manually: it applies the select condition to each key-value pair and reconstructs the object. For case-insensitive regex matching, use the i flag: test("pattern"; "i").
map() and map_values(): Transforming JSON
map(f) applies filter f to every element of an array and collects results into a new array — it is shorthand for [.[] | f]. map_values(f) applies f to every value of an object (or array) while preserving the keys. Together they cover the most common transformation patterns: reshaping objects, computing derived fields, encoding values, and filtering combined with transformation in a single readable expression.
# map — transform every array element
echo '[1,2,3,4,5]' | jq 'map(. * 2)'
# [2,4,6,8,10]
# map — reshape objects
echo '[{"first":"Alice","last":"Smith"},{"first":"Bob","last":"Jones"}]' \
| jq 'map({name: (.first + " " + .last)})'
# [{"name":"Alice Smith"},{"name":"Bob Jones"}]
# map — extract a field from each object (equivalent to [.[].field])
echo '[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]' \
| jq 'map(.name)'
# ["Alice","Bob"]
# map — add a computed field
echo '[{"price":100,"tax_rate":0.1},{"price":200,"tax_rate":0.15}]' \
| jq 'map(. + {total: (.price * (1 + .tax_rate))})'
# [{"price":100,"tax_rate":0.1,"total":110},{"price":200,"tax_rate":0.15,"total":230}]
# map — filter and transform combined (map + select)
echo '[{"name":"Alice","active":true},{"name":"Bob","active":false}]' \
| jq '[.[] | select(.active) | .name]'
# ["Alice"]
# Note: map(select(...)) drops non-matching elements from the result array
# map_values — transform object values, preserve keys
echo '{"a":1,"b":2,"c":3}' | jq 'map_values(. * 10)'
# {"a":10,"b":20,"c":30}
# map_values — apply a string transformation to all values
echo '{"name":" alice ","city":" London "}' | jq 'map_values(ltrimstr(" ") | rtrimstr(" "))'
# map_values — on arrays (same as map)
echo '[1,2,3]' | jq 'map_values(. + 100)'
# [101,102,103]
# Nested map — transform nested arrays
echo '{"teams":[{"members":[{"name":"Alice"},{"name":"Bob"}]}]}' \
| jq '.teams | map(.members | map(.name))'
# [["Alice","Bob"]]
# map with @base64 encoding
echo '["hello","world"]' | jq 'map(@base64)'
# ["aGVsbG8=","d29ybGQ="]
# map with type conversion
echo '["1","2","3"]' | jq 'map(tonumber)'
# [1,2,3]
echo '[1,2,3]' | jq 'map(tostring)'
# ["1","2","3"]map(select(condition)) does filter arrays, but it silently removes non-matching elements rather than outputting null for them — the resulting array is shorter than the input. When you need a same-length output with null for non-matches, use map(if condition then . else null end). For performance with large arrays, prefer [.[] | select(...) | transform] over separate map(select(...)) | map(transform) calls — combining them in a single pipeline avoids creating an intermediate array.
to_entries, from_entries, and with_entries
to_entries converts a JSON object into an array of {"key": k, "value": v} objects, enabling you to filter and transform key-value pairs using array operations. from_entries is the inverse — it converts an array of {"key": k, "value": v} objects back into a JSON object. with_entries(f) is shorthand for to_entries | map(f) | from_entries, applying a filter to each entry and reconstructing the object.
# to_entries — convert object to array of {key, value} pairs
echo '{"name":"Alice","age":30,"city":"London"}' | jq 'to_entries'
# [
# {"key":"name","value":"Alice"},
# {"key":"age","value":30},
# {"key":"city","value":"London"}
# ]
# from_entries — convert array of {key, value} back to object
echo '[{"key":"a","value":1},{"key":"b","value":2}]' | jq 'from_entries'
# {"a":1,"b":2}
# from_entries also accepts {name, value} and {Key, Value} formats
echo '[{"name":"x","value":10}]' | jq 'from_entries'
# {"x":10}
# with_entries — filter object keys
echo '{"name":"Alice","password":"secret","age":30}' \
| jq 'with_entries(select(.key != "password"))'
# {"name":"Alice","age":30}
# with_entries — rename keys (transform .key)
echo '{"firstName":"Alice","lastName":"Smith"}' \
| jq 'with_entries(.key |= gsub("(?<x>[A-Z])"; "_" + .x | ascii_downcase))'
# {"first_name":"Alice","last_name":"Smith"}
# with_entries — filter to only numeric values
echo '{"name":"Alice","score":95,"active":true,"count":10}' \
| jq 'with_entries(select(.value | type == "number"))'
# {"score":95,"count":10}
# with_entries — transform values while preserving keys
echo '{"a":1,"b":2,"c":3}' | jq 'with_entries(.value *= 100)'
# {"a":100,"b":200,"c":300}
# Invert a key-value mapping (swap keys and values)
echo '{"alice":"admin","bob":"editor","carol":"viewer"}' \
| jq 'to_entries | map({key: .value, value: .key}) | from_entries'
# {"admin":"alice","editor":"bob","viewer":"carol"}
# Build an object from two parallel arrays (zip)
jq -n '
["name","age","city"] as $keys |
["Alice",30,"London"] as $vals |
[$keys, $vals] | transpose | map({key: .[0], value: .[1]}) | from_entries
'
# {"name":"Alice","age":30,"city":"London"}
# Group array of objects by a field — build a lookup table
echo '[{"id":1,"dept":"eng"},{"id":2,"dept":"sales"},{"id":3,"dept":"eng"}]' \
| jq 'group_by(.dept) | map({key: .[0].dept, value: map(.id)}) | from_entries'
# {"eng":[1,3],"sales":[2]}The to_entries/from_entries pattern is the idiomatic way to manipulate object keys in jq — since jq has no direct "rename key" operation, converting to entries, transforming the .key field, and converting back is the standard approach. The |= update operator in with_entries(.key |= transform) mutates the key in place. Note that from_entries accepts objects with either key/value, name/value, or Key/Value fields — providing flexibility when the source data uses different field names.
reduce() and group_by(): Aggregating Data
reduce is jq's fold operation — it iterates over a stream and accumulates a result: reduce .[] as $x (initial; update). group_by(.field) groups array elements by a field value, returning an array of arrays. unique_by(.field) deduplicates by a field. sort_by(.field) sorts an array. These builtins replace common aggregation patterns that would otherwise require multiple passes.
# reduce — sum all values
echo '[1,2,3,4,5]' | jq 'reduce .[] as $x (0; . + $x)'
# 15
# reduce — product
echo '[1,2,3,4,5]' | jq 'reduce .[] as $x (1; . * $x)'
# 120
# reduce — build an object (frequency count)
echo '["a","b","a","c","b","a"]' \
| jq 'reduce .[] as $x ({}; .[$x] += 1)'
# {"a":3,"b":2,"c":1}
# reduce — merge array of objects
echo '[{"a":1},{"b":2},{"c":3}]' \
| jq 'reduce .[] as $x ({}; . * $x)'
# {"a":1,"b":2,"c":3}
# group_by — group array elements by a field
echo '[{"dept":"eng","name":"Alice"},{"dept":"sales","name":"Bob"},{"dept":"eng","name":"Carol"}]' \
| jq 'group_by(.dept)'
# [
# [{"dept":"eng","name":"Alice"},{"dept":"eng","name":"Carol"}],
# [{"dept":"sales","name":"Bob"}]
# ]
# group_by + map — summarize groups
echo '[{"dept":"eng","salary":100},{"dept":"sales","salary":80},{"dept":"eng","salary":120}]' \
| jq 'group_by(.dept) | map({dept: .[0].dept, count: length, total: map(.salary) | add})'
# [{"dept":"eng","count":2,"total":220},{"dept":"sales","count":1,"total":80}]
# unique_by — deduplicate by field
echo '[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"},{"id":1,"name":"Alice"}]' \
| jq 'unique_by(.id)'
# [{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]
# sort_by — sort by a field (ascending)
echo '[{"name":"Carol","age":28},{"name":"Alice","age":35},{"name":"Bob","age":22}]' \
| jq 'sort_by(.age)'
# sort_by — descending: reverse after sorting
echo '[{"score":80},{"score":95},{"score":72}]' \
| jq 'sort_by(.score) | reverse'
# min_by / max_by
echo '[{"name":"Alice","score":95},{"name":"Bob","score":72}]' \
| jq 'max_by(.score) | .name'
# "Alice"
# add — sum all elements (works on numbers, strings, arrays, objects)
echo '[1,2,3,4,5]' | jq 'add' # 15
echo '["a","b","c"]' | jq 'add' # "abc"
echo '[[1,2],[3,4]]' | jq 'add' # [1,2,3,4] (flatten one level)
echo '[{"a":1},{"b":2}]' | jq 'add' # {"a":1,"b":2} (merge)reduce is the escape hatch for aggregations that lack a dedicated builtin. The accumulator . inside the update expression refers to the current accumulated value, not the input — this is a common source of confusion. group_by always sorts the groups alphabetically by the grouping key, so the output order is deterministic. The add builtin is a concise alternative to reduce .[] as $x (null; . + $x) — it works correctly on null (returns null for an empty array), numbers, strings, arrays, and objects, automatically detecting the type.
jq in Shell Scripts and GitHub Actions
jq integrates naturally into shell scripts and CI pipelines. The key patterns are: using -r to strip quotes for shell variable assignment, --arg and --argjson to pass shell variables into filters safely, -e for conditional exit codes, and -n to construct JSON without any input. In GitHub Actions, jq processes workflow outputs, API responses, and config files directly in run steps.
# ── Shell script patterns ─────────────────────────────────────────
# Assign jq output to a shell variable (-r strips quotes)
NAME=$(jq -r '.name' data.json)
echo "Hello, $NAME"
# Read multiple values in one pass
read -r NAME AGE <<< $(jq -r '[.name, (.age|tostring)] | join(" ")' data.json)
# Pass shell variable into jq with --arg (always a string)
STATUS="active"
jq --arg status "$STATUS" '[.users[] | select(.status == $status)]' data.json
# Pass numeric shell variable with --argjson (parsed as JSON)
MIN_AGE=30
jq --argjson min "$MIN_AGE" '[.users[] | select(.age >= $min)]' data.json
# -e flag: exit code 1 when output is false/null — useful for conditionals
if jq -e '.features.darkMode' config.json > /dev/null 2>&1; then
echo "Dark mode is enabled"
fi
# -n flag: no input — construct JSON from scratch
jq -n --arg name "$NAME" --argjson score "$SCORE" \
'{"name": $name, "score": $score, "timestamp": (now | todate)}'
# Process multiple files — jq reads from stdin or file arguments
jq '.id' file1.json file2.json file3.json # outputs each file's .id
# -s flag: slurp — read all inputs into one array
jq -s '.[0] * .[1]' base.json override.json # merge two files
# Loop over JSON array in bash
while IFS= read -r line; do
echo "Processing: $line"
done < <(jq -rc '.items[]' data.json)
# ── GitHub Actions patterns ────────────────────────────────────────
# Extract a value and set as step output
- name: Get version
id: version
run: |
VERSION=$(jq -r '.version' package.json)
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
# Filter GitHub API response
- name: Get latest release
run: |
LATEST=$(curl -s https://api.github.com/repos/owner/repo/releases/latest \
| jq -r '.tag_name')
echo "Latest release: $LATEST"
# Conditionally skip based on JSON config
- name: Check feature flag
run: |
if jq -e '.deploy.staging' config.json > /dev/null; then
echo "Deploying to staging"
fi
# Build JSON payload for API call
- name: Create deployment
run: |
PAYLOAD=$(jq -n \
--arg env "production" \
--arg ref "${{ github.sha }}" \
'{"environment": $env, "ref": $ref, "auto_merge": false}')
curl -s -X POST https://api.example.com/deployments \
-H "Content-Type: application/json" \
-d "$PAYLOAD"
# Parse matrix strategy output
- name: Process matrix results
run: |
jq -r '.[] | select(.status == "failed") | .name' results.jsonAlways use --arg (not shell interpolation) to pass shell variables into jq filters — direct interpolation like jq ".name == \"$NAME\"" breaks when $NAME contains quotes, spaces, or special characters, creating both bugs and security vulnerabilities. The --argjson flag is essential for numeric comparisons: passing 30 with --arg creates a string "30", and select(.age >= "30") does string comparison, not numeric. Use jq -n '{"key": $ENV.MY_VAR}' to read environment variables directly in jq without shell argument passing. See the GitHub Actions JSON guide for more CI/CD patterns.
Key Terms
- filter
- In jq, a filter is an expression that takes a JSON value as input and produces one or more JSON values as output. Every jq program is a filter — from the simplest
.(identity, returns input unchanged) to complex multi-step pipelines. Filters are composable: the output of one filter becomes the input of the next via the pipe operator|. Filters that produce multiple outputs (like.[]) stream each value independently. A filter that produces no output (likeselect(false)) effectively removes values from the stream. jq ships with many builtin filters:length,keys,values,type,has(key),in(object),map(f),select(cond),empty,error,add,any,all,flatten,range,floor,sqrt,ascii_downcase,ltrimstr,split,join,indices,inside,contains,limit,first,last,nth,recurse,env,now, and format strings. - pipe (|)
- The pipe operator
|connects two filters so that the output of the left filter becomes the input to the right filter. If the left filter produces multiple outputs (a stream), the right filter is applied to each output independently..users[] | .namefirst iterates over all users (producing multiple objects), then extracts thenamefield from each — the pipe connects these two operations. Pipes are the primary composition mechanism in jq, analogous to Unix shell pipes but operating on JSON values rather than text bytes. Unlike shell pipes, jq pipes operate in-process with no serialization overhead. Parentheses control grouping:(.a, .b) | .capplies.cto both.aand.boutputs. - select()
- A jq builtin filter that passes its input through unchanged if the condition expression is truthy, and produces no output if the condition is false or null. It is the primary way to filter elements in a stream or array.
select()does not transform its input — it only decides whether to pass it through or discard it. Used with.[]to filter arrays:[.[] | select(.active)]keeps only active elements. The condition can use any jq expression: comparisons (==,!=,>,<,>=,<=), boolean operators (and,or,not), type checks (type == "string"), and builtins (startswith,endswith,contains,test,has,in). An emptyselect()call is equivalent toselect(true)— it passes everything through. - map()
- A jq builtin that applies a filter to every element of an array and collects the results into a new array.
map(f)is exactly equivalent to[.[] | f]— it iterates, applies, and collects. The filterfcan be any jq expression: field extraction (map(.name)), arithmetic (map(. * 2)), object construction (map({ id, name } )), or complex pipelines (map(select(.active) | .name)).map_values(f)appliesfto each value of an object (preserving keys) or each element of an array (same asmap). Unlikemap,map_valuesalways produces output of the same length as input — it does not filter. Both builtins are preferred over manual[.[] | ...]patterns for readability. - to_entries
- A jq builtin that converts a JSON object into an array of
{"key": k, "value": v}objects, one per key-value pair in the original object. The inverse operation isfrom_entries, which reconstructs a JSON object from such an array.to_entriesenables key-based filtering and transformation using array operations likeselect()andmap():to_entries | map(select(.key | startswith("x_"))) | from_entrieskeeps only keys starting withx_.with_entries(f)is shorthand forto_entries | map(f) | from_entries.from_entriesaccepts objects withkey/value,name/value, orKey/Valuefields. When applied to an array,to_entriesproduces{"key": index, "value": element}objects using numeric indices as keys. - @base64 format string
- One of jq's format string builtins, prefixed with
@, used to encode string values into specific formats.@base64encodes a string as Base64;@base64ddecodes Base64 back to a string. Other format strings:@uri(percent-encodes for URLs),@csv(formats an array as a CSV row with proper quoting),@tsv(tab-separated values),@html(escapes HTML special characters),@json(encodes as a JSON string — useful for embedding JSON inside JSON),@sh(shell-escapes for safe use in shell commands). Format strings are applied inside string interpolation:{""\(.data | @base64)""}or as a standalone filter in a pipeline:.data | @base64. Always combine with-rto get a raw string output rather than a JSON-quoted string.
FAQ
How do I extract a field from JSON with jq?
Use dot notation: echo '{"name":"Alice"}' | jq '.name' outputs "Alice". For nested fields, chain dots: jq '.address.city'. To remove the surrounding quotes for use in a shell script, add -r: jq -r '.name' outputs Alice without quotes. For array elements, use index notation: jq '.[0]' for the first element, jq '.[-1]' for the last. To extract multiple fields at once into a new object, use object construction: jq '{ name: .name, age: .age }'. For fields that may be absent, use the optional operator to suppress errors: jq '.optionalField?' returns null instead of an error when the field does not exist.
How do I filter a JSON array with jq?
Use .[] to iterate, pipe to select(condition) to keep matching elements, then wrap in [ ] to collect results: jq '[.[] | select(.age > 30)]'. The select() filter passes through elements where the condition is truthy and discards everything else. Common conditions: select(.status == 'active') for equality, select(.score >= 90) for comparisons, select(.name | startswith('A')) for string prefix, select(.name | test('pattern')) for regex, and select(.tags | contains(['admin'])) for array containment. Chain a transformation after select to reshape the output in the same pipeline: [.[] | select(.active) | {id, name}].
How do I transform JSON with jq?
Use map(f) to transform every element of an array: jq 'map({id, fullName: (.first + " " + .last)})' reshapes each object. Use object construction { key: expr } to build new shapes from existing fields. Use string interpolation "\.(.field)" to build strings: jq '.[] | "\(.name): \(.score)"'. Use the update operator |= to modify fields in place: jq '.price |= . * 1.1' applies a 10% increase. The + operator merges objects: jq '. + {computed: (.a + .b)}' adds a new field. Use with_entries(.value |= transform) to transform all values of an object while preserving its keys.
How do I pretty print JSON with jq?
jq pretty-prints by default — pipe any JSON through jq '.' to reformat it with 2-space indentation: echo '{"b":2,"a":1}' | jq '.'. To pretty-print a file: jq '.' data.json. For compact (minified) single-line output, use -c: jq -c '.' data.json. To sort keys alphabetically, use -S: jq -S '.' data.json. For tab indentation: jq --tab '.'. For custom indentation width: jq --indent 4 '.'. These flags combine freely: jq -Sc '.' outputs compact sorted JSON. Pretty-printing is lossless — the output is always valid JSON with identical data. Use jq '.' file.json > file-pretty.json to write the formatted output to a new file.
How do I get the keys of a JSON object with jq?
Use the keys builtin: echo '{"c":3,"a":1,"b":2}' | jq 'keys' returns ["a","b","c"] (sorted alphabetically). For insertion-order keys, use keys_unsorted. To get only values (no keys), use values. To check if a specific key exists, use has(): jq 'has("name")' returns true or false. To count keys: jq 'keys | length'. To iterate over key-value pairs and format them, use to_entries: jq 'to_entries[] | "\(.key)=\(.value)"'. To filter an object to only specific keys: jq 'with_entries(select(.key == "name" or .key == "age"))'. To delete a key: jq 'del(.password)'.
How do I merge two JSON objects with jq?
Use the * operator for recursive merge: jq -n '{"a":1,"b":2} * {"b":3,"c":4}' returns {"a":1,"b":3,"c":4} — right-hand values win for duplicate keys, and nested objects are merged recursively rather than replaced. To merge from two files: jq -s '.[0] * .[1]' base.json override.json. For a shallow merge (no deep recursion), use +: jq -n '{"a":1} + {"b":2}'. The difference between * and + matters for nested objects: + replaces a nested object entirely, while * merges recursively. To merge an array of objects into one: jq 'reduce .[] as $x ({}; . * $x)' or the shorthand jq 'add' (which uses +, not *).
How do I use jq in a shell script?
Assign output to a variable with -r to strip quotes: NAME=$(jq -r '.name' data.json). Pass shell variables into jq safely with --arg (string) or --argjson (parsed JSON for numbers/booleans): jq --arg status "$STATUS" 'select(.status == $status)' — never use shell interpolation inside jq filters as it breaks on special characters. Use -e for conditional exit codes: jq -e '.active' config.json && echo "active" — exits 1 when output is false or null. Use -n to construct JSON from shell variables without an input file: jq -n --arg name "$NAME" --argjson score "$SCORE" '{"name": $name, "score": $score}'. Loop over array elements with while read -r line; do ...; done < <(jq -rc '.items[]' data.json).
How do I output CSV from JSON with jq?
Use the @csv format string with -r: jq -r '.[] | [.name, .age, .city] | @csv' data.json. The @csv format automatically double-quotes strings, escapes embedded quotes, and leaves numbers unquoted — the output is spec-compliant RFC 4180 CSV. To add a header row, prepend it as an array: jq -r '["Name","Age","City"], (.[] | [.name, .age, .city]) | @csv'. For tab-separated output, use @tsv instead of @csv. Handle null or missing values with the alternative operator: jq -r '.[] | [(.name // ""), (.age // 0)] | @csv'. The input to @csv must be a flat array — numbers and strings only; nested arrays or objects cause an error. Pipe to a file to save: jq -r '... | @csv' data.json > output.csv. See the JSON to CSV tutorial for more conversion approaches.
Further reading and primary sources
- jq Manual (development version) — Complete official reference for all jq builtins, filters, format strings, and advanced features
- jq Cookbook — Community-maintained collection of practical jq recipes for common JSON processing tasks
- jq Playground — Interactive in-browser jq editor for testing filters against JSON without installing jq
- jq GitHub repository — Source code, issue tracker, and release downloads for the jq command-line JSON processor
- JSON Streaming and NDJSON — How to process large JSON files line by line using NDJSON and streaming parsers