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

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

Common Errors (and the fix)

📋 Quick Reference

FilterExampleWhat it does
Identityjq '.'Input unchanged, pretty-printed
Fieldjq '.name'Value of a key
Nestedjq '.a.b'Chain keys to go deeper
Indexjq '.[0]'One element (zero-based)
Iteratejq '.[]'Each element separately
Pipejq '.[] | .name'Feed one filter into the next
Filterjq 'select(.age > 30)'Keep only matching input
Transformjq 'map(.price * 2)'Apply to each, return array
Rawjq -r '.name'Strip the JSON quotes
Keysjq 'keys'Object keys, sorted array
Lengthjq 'length'Array len / string len / key count
Buildjq '{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!

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

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].