Processing JSON with jq
Reviewed & published by Brayan K
By the end of this lesson you'll be able to slice, filter, and reshape JSON straight from the terminal with jq — pulling out fields, iterating arrays, filtering with conditions, and feeding a live API response from curl into a clean, scriptable result.
Part of the free Command Line course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- Use the identity filter . and access fields with .key and nested .a.b
- Iterate arrays with .[] and index with .[0] / .[2]
- Pipe filters with | and filter with select(.age > 30)
- Transform every element with map(.price * 2) and strip quotes with -r
- Inspect data with keys (sorted) and length
- Build new objects like {name: .name, id: .id} and combine jq with curl
1. The Identity Filter & Accessing Fields
jq is a dedicated JSON processor with its own little filter language — it is not bash. You pipe JSON into it and give it a filter wrapped in single quotes (the quotes stop the shell from touching it). The simplest filter is ., the identity filter: it returns the input unchanged and pretty-prints it. To pull out a field, write .key — for example .name. Strings come back with their JSON quotes.
# jq is a dedicated JSON processor. You hand it JSON on stdin and a
# FILTER (in jq's own little language) inside single quotes.
# Single quotes protect the filter from the shell.
# The identity filter '.' returns the input unchanged — and jq
# pretty-prints (indents + colours) it by default:
echo '{"name":"Ada","age":36}' | jq '.'
# Access a field with .key — note the leading dot:
echo '{"name":"Ada","age":36}' | jq '.name'
# Strings come back WITH quotes (that is valid JSON):
echo '{"name":"Ada","age":36}' | jq '.age'2. Reaching Into Nested Objects
JSON nests objects inside objects. To dig in, just chain the keys: .a.b.c. So .address returns the whole address object, and .address.city reaches one level deeper to the city inside it. Each dot steps down one level.
# Reach into nested objects by chaining keys: .a.b.c
# Here the address is an object nested inside the person.
echo '{"name":"Ada","address":{"city":"London","zip":"SW1"}}' \
| jq '.address'
echo '{"name":"Ada","address":{"city":"London","zip":"SW1"}}' \
| jq '.address.city'3. Arrays: Index With .[0] and Iterate With .[]
For arrays, .[n] grabs a single element by its zero-based index — .[0] is the first, .[2] the third. To process every element, use .[]: it iterates, emitting each element as its own separate result. Put it after a field — .colors[] — to walk an array that lives inside an object.
# Index into an array with .[n] (zero-based):
echo '["red","green","blue"]' | jq '.[0]'
echo '["red","green","blue"]' | jq '.[2]'
# Iterate over EVERY element with .[] — each element comes out
# as its own separate result (one per line):
echo '["red","green","blue"]' | jq '.[]'
# .[] also works after a field, to walk an array inside an object:
echo '{"colors":["red","green","blue"]}' | jq '.colors[]'4. Raw Output With -r
By default jq prints strings as valid JSON, so a name comes out as "Ada" — quotes and all. The -r (raw) flag strips those quotes and prints Ada. That's exactly what you want when piping a value into another command or capturing it in a shell variable. Combine -r with .[] to get a clean, plain list.
# By default jq prints strings WITH their JSON quotes:
echo '{"name":"Ada"}' | jq '.name' # -> "Ada"
# The -r (raw) flag strips the quotes — perfect for piping a value
# into another command or capturing it in a shell variable:
echo '{"name":"Ada"}' | jq -r '.name' # -> Ada
# Iterating + raw gives you a clean, plain list of values:
echo '["red","green","blue"]' | jq -r '.[]'5. Piping Filters: select and map
Inside a filter, jq has its own pipe | that feeds one filter's output into the next. select(condition) passes its input through only when the condition is true, so .[] | select(.age > 30) keeps just the matching records. map(f) applies a filter to every element of an array and collects the results into a new array — map(.price * 2) doubles each price.
# Inside jq you can PIPE one filter into the next with '|'
# (this is jq's own pipe, separate from the shell's pipe).
# Iterate the array, then keep only objects where age > 30:
echo '[{"name":"Ada","age":36},{"name":"Bob","age":24}]' \
| jq '.[] | select(.age > 30)'
# Combine with field access to pull just the matching names (raw):
echo '[{"name":"Ada","age":36},{"name":"Bob","age":24}]' \
| jq -r '.[] | select(.age > 30) | .name'
# map(f) applies a filter to EVERY element and returns a new array.
# Here we double each price:
echo '[{"price":2},{"price":5}]' | jq 'map(.price * 2)'6. keys, length, Building Objects & curl
keys returns an object's keys as a sorted array; length gives an array's element count, a string's character count, or an object's number of keys. You can build a brand-new object with {name: .name, id: .id}, keeping only the fields you care about. And the payoff: pipe a live response from curl straight into jq, e.g. curl -s URL | jq '.results[].name'.
# keys -> the object's keys, as a SORTED array:
echo '{"name":"Ada","age":36,"city":"London"}' | jq 'keys'
# length -> array length / string length / number of object keys:
echo '["a","b","c","d"]' | jq 'length' # -> 4
echo '{"name":"Ada","age":36}' | jq 'length' # -> 2 (two keys)
# Build a NEW object with {key: filter, ...}. Pick out just two fields:
echo '{"name":"Ada","age":36,"city":"London"}' \
| jq '{name: .name, age: .age}'
# Real-world: pipe a live API response from curl straight into jq.
# -s keeps curl quiet; the single-quoted filter is safe from the shell:
# curl -s https://api.example.com/users | jq '.results[].name'🎯 Your Turn: pull a nested field, raw
Fill in the blank marked ___ using the # 👉 hint, then run it and check the Output panel matches.
# 🎯 YOUR TURN — fill in the blanks, then run it.
# Goal: pull the city out of the nested address object as RAW text.
# 1) Add the flag that strips the surrounding quotes from strings.
echo '{"user":{"name":"Ada","address":{"city":"London"}}}' \
| jq ___ '.user.address.city' # 👉 replace ___ with -r
# ✅ Expected output:
# London🎯 Your Turn: iterate and filter
Two blanks: the filter that iterates an array, and the comparison that means "18 or more". Then run it.
# 🎯 YOUR TURN — fill in the two blanks, then run it.
# Goal: from the array, keep only people whose age is 18 or more.
echo '[{"name":"Ada","age":36},{"name":"Kai","age":12}]' \
| jq '___ | select(.age ___ 18)'
# 👆 replace first ___ with .[] (iterate the array)
# 👆 replace second ___ with >=
# ✅ Expected output:
# {
# "name": "Ada",
# "age": 36
# }Pro Tips
- 💡 Always single-quote the filter: jq '.[] | select(.age > 30)'. Without quotes the shell mangles $, *, spaces, and parentheses before jq sees them.
- 💡 Reach for -r when piping onward: raw output drops the JSON quotes so a value slots cleanly into $(...) or another command.
- 💡 .[] streams, map() collects: use .[] with select for a flowing list; use map() when you want the result back as one array.
- 💡 Build before you read: jq has a playground at jqplay.org — paste sample JSON and tweak the filter live until the output is right.
Common Errors (and the fix)
- "jq: error: syntax error" from an unquoted filter — the shell expanded part of it. Wrap the whole filter in single quotes: jq '.name'.
- Forgetting the leading dot — it is .name, not name. A field path always starts with a dot.
- "Cannot index array with \"name\"" — you used .name on an array. Iterate first: .[] | .name (or index it, .[0].name).
- Output still has quotes — you wanted plain text but left off -r. Add it: jq -r '.name'.
- "jq: command not found" — jq isn't installed. Install it (for example brew install jq or apt install jq), then retry.
📋 Quick Reference
| Filter | Example | What it does |
|---|---|---|
| Identity | jq '.' | Input unchanged, pretty-printed |
| Field | jq '.name' | Value of a key |
| Nested | jq '.a.b' | Chain keys to go deeper |
| Index | jq '.[0]' | One element (zero-based) |
| Iterate | jq '.[]' | Each element separately |
| Pipe | jq '.[] | .name' | Feed one filter into the next |
| Filter | jq 'select(.age > 30)' | Keep only matching input |
| Transform | jq 'map(.price * 2)' | Apply to each, return array |
| Raw | jq -r '.name' | Strip the JSON quotes |
| Keys | jq 'keys' | Object keys, sorted array |
| Length | jq 'length' | Array len / string len / key count |
| Build | jq '{n: .name}' | Construct a new object |
Mini-Challenge: summarise a product list
No blanks this time — just a brief and some hints. Given a JSON array of products, count them, list their names cleanly, and rebuild the pricier ones into trimmed objects. Check your output against the example in the comments.
# 🎯 MINI-CHALLENGE: summarise a list of products
#
# Given this JSON on stdin:
# [{"name":"Pen","price":2},{"name":"Mug","price":8},{"name":"Cap","price":15}]
#
# 1. Print how many products there are. (hint: length)
# 2. Print just the product NAMES, one per line, with no quotes.
# (hint: .[] | .name and -r)
# 3. Keep only products priced over 5, and for each build a new object
# {name: .name, price: .price}.
# (hint: .[] | select(.price > 5) | {...})
#
# ✅ For step 1 you should see: 3
# your jq filters here🎉 Lesson Complete!
- ✅ jq is a dedicated JSON processor with its own filter language — wrap filters in single quotes
- ✅ . is identity; .key and .a.b read fields; .[0] indexes and .[] iterates
- ✅ Pipe filters with |; select(.age > 30) filters and map(.price * 2) transforms
- ✅ -r strips quotes; keys gives a sorted key array; length counts
- ✅ Build new objects with {name: .name, id: .id} and feed curl -s URL | jq '...'
Practice quiz
What is jq?
- A dedicated command-line JSON processor with its own filter language
- A bash built-in command
- A web browser
- A JavaScript runtime
Answer: A dedicated command-line JSON processor with its own filter language. jq is a standalone tool for slicing, filtering, and transforming JSON. Its filter language is its own — it is not bash.
Which filter is the identity filter that passes the input through unchanged (and pretty-prints it)?
- @
- *
- .
- id
Answer: .. The single dot . is the identity filter: it returns the input as-is and jq pretty-prints it by default.
How do you access the field named name on a JSON object?
- name()
- .name
- get(name)
- ->name
Answer: .name. A field is accessed with .name, for example echo '{"name":"Ada"}' | jq '.name'.
What does the filter .[] do?
- Deletes the array
- Returns the array length
- Sorts the array
- Iterates over the elements of an array (or values of an object)
Answer: Iterates over the elements of an array (or values of an object). .[] iterates: it produces each element of an array as a separate output value.
What does the -r flag do?
- Outputs raw strings without surrounding quotes
- Reads from a file
- Repeats the filter
- Reverses the output
Answer: Outputs raw strings without surrounding quotes. -r (raw output) prints string results without the surrounding double quotes, which is ideal for shell pipelines.
Why are jq filters usually wrapped in single quotes?
- It is only a style preference with no effect
- To stop the shell from expanding or mangling characters like $, *, and spaces in the filter
- To make jq run faster
- Single quotes are required to define a variable
Answer: To stop the shell from expanding or mangling characters like $, *, and spaces in the filter. Single quotes protect the filter from the shell so jq receives it verbatim.
What does keys return when given a JSON object?
- The number of keys
- The values only
- The first key as a string
- The object's keys as a sorted array
Answer: The object's keys as a sorted array. keys returns the object's keys as an array, sorted alphabetically.
What does length return for a JSON array?
- The first element
- Always 1
- The number of elements in the array
- The array reversed
Answer: The number of elements in the array. For an array, length is the element count; for a string it is the character count; for an object it is the number of keys.
Which filter keeps only the items where age is greater than 30?
- filter(age, 30)
- select(.age > 30)
- where .age > 30
- keep(.age)
Answer: select(.age > 30). select(condition) passes its input through only when the condition is true, e.g. select(.age > 30).
What does map(.price * 2) do to an input array?
- Returns a new array with each element's price doubled
- Returns only the first price
- Sorts the array by price
- Deletes the price field
Answer: Returns a new array with each element's price doubled. map(f) applies the filter f to every element of an array and collects the results into a new array.
Continue this course
- Previous: HTTP from the Terminal: curl and wget
- Next: Package Managers — Install and update software with apt, dnf, pacman and Homebrew
- Quick reference: Command Line cheat sheet
Frequently asked questions
Is jq part of bash, or a separate program?
jq is a separate, dedicated program with its own filter language. You usually feed JSON into it through a shell pipe (for example curl ... | jq '...'), but the filter you write inside the quotes is jq syntax, not bash. That is why .name, .[], select(), and map() look nothing like shell commands.
Why do my jq filters need single quotes around them?
The single quotes protect the filter from the shell. Characters like $, *, spaces, and parentheses are special to bash; if you leave the filter unquoted the shell may expand or mangle it before jq ever sees it. Wrapping the whole filter in single quotes — jq '.[] | select(.age > 30)' — passes it through verbatim.
What is the difference between jq '.name' and jq -r '.name'?
Both read the name field, but plain jq prints a string as valid JSON, so you get "Ada" with the quotes. The -r (raw) flag strips those quotes and prints Ada. Use -r when you want to pipe the value into another command or capture it in a shell variable.
What does the | mean inside a jq filter — is it the shell pipe?
No. jq has its OWN pipe operator |, used INSIDE the single-quoted filter to feed one filter's output into the next, like .[] | select(.age > 30) | .name. It is separate from the shell's | that connects whole programs. A command can use both: curl uses the shell pipe to reach jq, and jq uses its own pipe internally.
What is the difference between .[] and map()?
Both touch every element of an array. .[] is a generator: it emits each element as a separate result (great with select and -r for streaming output). map(f) is a collector: it applies the filter f to each element and gathers the results back into a single new array, so map(.price * 2) on [2,5] returns [4,10].