Index Ai Academy
Phase 7 · Software engineering · Building software

JSON on the command line: curl and jq

Call APIs with curl, fail on HTTP errors, and pull values out of JSON with jq paths and select.

About 25 minutes · A free account opens the whole lesson

Create a free account Try part of it here

Already have an account? Sign in to open this lesson

Why it matters

APIs speak JSON, and engineers poke at them constantly: checking what an endpoint really returns, pulling one field out of a large response, or spotting the product that's out of stock before the dashboard does. curl fetches, jq reads JSON the way grep reads text, and together they let you inspect an API in seconds without writing a script or opening Postman. This lesson works against a simulated shop API, so every command here is safe to try, and nothing you send leaves the page.

What you'll be able to do

  • Call an API from the terminal with curl, and make HTTP errors fail the command
  • Pull values out of JSON with jq paths, iterators and raw output
  • Filter JSON with select and chain filters with pipes
  • Combine curl, jq and the text tools to answer a question about an API in one line

What it covers

  1. curl: HTTP from the terminal
  2. jq: paths through JSON
  3. select: keep the items that match
  4. When jq, when Python
Quick check
Before any explaining, a guess. An API returns 3,000 lines of JSON and you only want each product's SKU. What's the quickest tool?
  1. grep for the word sku
  2. jq, which picks fields out of JSON
  3. Copy it into a spreadsheet
  4. Read it in the browser
Show the answer

jq, which picks fields out of JSON

jq understands JSON's structure, so .products[].sku gives exactly the SKUs. grep sees only lines of text and breaks as soon as the formatting changes.

curl: HTTP from the terminal

curl URL sends a GET request and prints the response body. A few flags cover daily use:

  • -s (silent) hides the progress meter, which you want in pipelines;
  • -f makes HTTP errors like 404 or 500 fail the command, instead of printing the error page as if it worked;
  • -H 'Authorization: Bearer ...' adds a header; read the token from an environment variable, never type it;
  • -X POST -d '{"q": "tent"}' sends a body, usually with -H 'Content-Type: application/json' so the server knows how to read it.

Without -f, a script that fetches a missing order happily carries on with an error message as its "data". With it, curl exits with a failure code, so && chains stop and scripts can react.

When something goes wrong, curl can show you more. -i prints the response's status line and headers above the body, so you can see a 429 Too Many Requests, a redirect you didn't expect, or a missing Content-Type, and -v shows the whole conversation, request headers included, which is the fastest way to find out whether your Authorization header was sent at all. In scripts, add --max-time 10 so a hung server can't hang your script with it.

Quick check
Why add -f to curl in a script that uses the response?
  1. So the response body is always formatted as JSON
  2. So an HTTP error makes curl fail, not pass on the error page
  3. So it hides the progress meter
  4. So curl follows redirects
Show the answer

So an HTTP error makes curl fail, not pass on the error page

With -f, a 404 or 500 makes curl exit with an error code, so the next command doesn't run on an error message as if it were data.

jq: paths through JSON

jq takes a filter and applies it to JSON, either from a file named after the filter or, more often, from a pipe. Think of a filter as a path through the nested dictionaries and lists, written left to right. The filter . is the whole input, pretty-printed. .products is a key, .products[0] the first item, and .products[] every item, one after another. Paths chain: .products[].sku gives each product's SKU. Add -r (raw) to print strings without their quotes, which is what you want before handing them to other tools.

curl -s https://api.trailhead.test/products | jq '.products | length'
curl -s https://api.trailhead.test/products | jq -r '.products[].sku'

Inside a filter, | passes values along, just like the shell's pipe does for lines: .products | length takes the list and counts it. Quote filters in single quotes so the shell leaves them alone.

Two more filters help when an API is new to you. keys lists an object's keys, and length counts a list's items or an object's keys. A key that isn't there gives null rather than an error, which is convenient for exploring but means a typo in a field name fails quietly: if a filter prints null, check the spelling against keys before suspecting the data.

Try it now

A simulated shop API answers at https://api.trailhead.test. Each task is checked by what your command prints.

Quick check
data.json holds {"order": "A100", "items": [{"sku": "FLEECE-M"}, {"sku": "MUG-01"}]}. What does this print?
jq -r '.items[1].sku' data.json
  1. MUG-01
  2. FLEECE-M
  3. "MUG-01"
  4. ["FLEECE-M", "MUG-01"]
Show the answer

MUG-01

Indexes start at 0, so items[1] is the second item, and -r prints its SKU without quotes.

In your own words
What does -f do for curl, and what does jq's .items[].sku mean?

Write a sentence or two, then compare it with a good answer in the lesson.

select: keep the items that match

select(CONDITION) keeps an item when the condition is true and drops it otherwise, like grep for JSON. Compare a field with ==, !=, <, >, <= or >=:

jq -r '.products[] | select(.stock == 0) | .sku'
jq -r '.products[] | select(.price < 100) | .name' data/products.json

Read the first left to right: take each product, keep those with no stock, print their SKUs. The second reads a local file instead of a response, which is handy when you've saved an API's output to work on offline or want the same answer every time while you build up a longer filter step by step. Because jq's output is lines, the rest of your toolkit still applies: | sort, | wc -l and | head all work on jq's results.

The order of the stages matters. Filter whole items first and pick the field last: once you've taken .sku, only a string is left, and a string has no price to compare. The same rule holds in Python, where you'd write a list comprehension over the products with the condition on the product and the field in the result. Full jq also offers and, or, map, group_by and much more; the simulated terminal here supports the everyday core.

Quick check
With data/products.json (Ridge tent 189, Trail mug 12, Aspen fleece 79, in that order), what does this print?
jq -r '.products[] | select(.price >= 79) | .sku' data/products.json
  1. FLEECE-M only
  2. TENT-2P, then FLEECE-M
  3. TENT-2P only
  4. All three SKUs
Show the answer

TENT-2P, then FLEECE-M

>= includes 79, so the tent (189) and the fleece (79) pass, printed in the file's order.

Quick check
A reminder from the HTTP lesson. In Python, what does response.json() give you for this kind of response?
  1. A table with one row per product
  2. Dicts and lists, the structure jq walks
  3. The HTTP status code
  4. A single long string of JSON text
Show the answer

Dicts and lists, the structure jq walks

JSON objects become dictionaries and arrays become lists, so .products[0].sku in jq is data["products"][0]["sku"] in Python.

When jq, when Python

jq shines for looking: one field from a big response, a count, a filter, a quick check before a deploy. Its language goes much further than this lesson, with functions, maps and grouping, but once a jq filter needs several lines, the same work is usually clearer in Python with requests and ordinary dictionaries, plus tests. A reasonable split: jq for questions you ask by hand today, Python for anything that runs on a schedule or feeds another system, where you want error handling, retries and a test that proves it still works next month.

Two habits keep command-line API work safe. Keep tokens in environment variables, as in -H "Authorization: Bearer $API_TOKEN", so they never land in your shell history or a pasted screenshot. And use -f whenever another command depends on the response, so errors stop the chain instead of flowing into it.

A good routine for an unfamiliar API: fetch one response and pretty-print it with jq ., look at its shape with keys and length, try the path to the field you need on a single item, then add [] and select to handle them all. Save a sample response to a file (curl's -o sample.json does it) while you explore, so you aren't calling a rate-limited API dozens of times while you get a filter right.

Quick check
Which of these leaks a secret?
  1. Reading the key from $API_TOKEN in the command
  2. Typing the API key itself after Bearer in the command
  3. Using -f so errors fail
  4. Piping the response into jq
Show the answer

Typing the API key itself after Bearer in the command

A key typed into a command is saved in your shell history and shows up in screenshots and logs. A variable keeps the value out of the command line.

In your own words
What does select do, and which habit leaks a secret?

Write a sentence or two, then compare it with a good answer in the lesson.

Practice what you just read

  • a practice exercise that checks your code
  • a 6-question quiz
  • 5 review cards for spaced repetition

Create a free account

Try a quiz question

What does jq -r '.users[].email' print for a response with three users?

  1. Each email on its own line, no quotes
  2. One JSON list of three emails
  3. The number 3
  4. The whole response, pretty-printed

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson