Reading and inspecting JSON from the command line

Fetch, pretty-print and filter JSON with curl and jq, reshape it for a report, and know what a JSONC config file allows.

Fetch and format

# get it and look at it
curl -s https://api.example.com/orders | jq .

# a compact single line, useful for logs and diffs
curl -s https://api.example.com/orders | jq -c .

# the same with python, if jq is not installed
curl -s https://api.example.com/orders | python -m json.tool

# send a body and read the response
curl -s -X POST https://api.example.com/orders \
  -H 'Content-Type: application/json' \
  -d '{"sku":"A-1","quantity":2}' | jq .

# see status and body separately: -w writes to stdout after the body
curl -s -o /tmp/body.json -w '%{http_code}' https://api.example.com/orders
jq . /tmp/body.json
  • -s silences curl's progress meter, which otherwise corrupts the pipe. -S keeps the error messages.
  • jq . is the identity filter: it parses and re-emits, which both validates and pretty-prints.
  • A parse error from jq is your first signal that the response is HTML - a redirect to a login page is the classic case.
  • Pipe through jq -S to sort keys, so two responses can be compared with diff.
# what did the server actually send?
curl -sI https://api.example.com/orders | grep -i content-type

# and what does the error look like when the request is wrong?
curl -s https://api.example.com/orders/999 | jq .
💡
Reach for jq before reaching for a debugger. Most 'the API is broken' reports turn out to be a response shape that does not match the code's expectation, and jq . | head -40 shows that in one line.

Selecting and reshaping

# navigate
jq '.data.orders[0].id'
jq '.data.orders | length'
jq '.data.orders[] | .id'                # one per line
jq -r '.data.orders[] | .id'             # raw strings, no quotes

# filter
jq '.data.orders[] | select(.status == "paid")'
jq '.data.orders[] | select(.total > 1000 and .currency == "GBP")'
jq '[.data.orders[] | select(.status != "cancelled")] | length'

# shape the output
jq '.data.orders[] | {id, total, currency}'
jq '{count: (.data.orders | length), total: ([.data.orders[].total] | add)}'

# transform keys and values
jq '.data.orders | map(.total = (.total / 100))'
jq '.data.orders | sort_by(.createdAt) | reverse | .[0:5]'
jq '.data.orders | group_by(.status) | map({status: .[0].status, n: length})'
FilterMeaning
.a.bNavigate into an object
.items[]Iterate an array
.[0:5]Slice an array
select(f)Keep items where f is true
map(f)Apply f to every element
{a, b}Build a new object
length, add, uniqueAggregate over an array
to_entries, from_entriesConvert between object and pairs
# exit status drives shell logic
if curl -s "$URL" | jq -e '.data.orders | length > 0' > /dev/null; then
  echo "orders present"
else
  echo "empty or unavailable"
fi

# export a table
jq -r '.data.orders[] | [.id, .status, (.total / 100)] | @csv' orders.json

# merge several files into one array
jq -s '.' page-1.json page-2.json page-3.json

# interpolate a shell value safely, without string concatenation
jq --arg id "$ORDER_ID" '.data.orders[] | select(.id == $id)' orders.json

JSONC and JSON5 config files

Several tools accept a relaxed dialect for configuration. They are not JSON, and a parser that complies with the specification will reject them - which is exactly what happens when you copy a JSONC snippet into an API request body.

DialectAllowsUsed by
JSONNothing beyond the specAPIs, data interchange
JSONCComments, trailing commasVS Code settings, tsconfig
JSON5Comments, trailing commas, unquoted keys, single quotes, hex, InfinitySome build configs
Relaxed JSONVaries by parserAnything that says so explicitly
// a JSONC file: valid for the editor, invalid for JSON.parse
{
  // the compiler options that matter for this package
  "compilerOptions": {
    "strict": true,
    "noEmit": true,
  },
}

// reading it in code needs a tolerant parser
import { parse } from "jsonc-parser";
const config = parse(await readFile("tsconfig.json", "utf8"));
# strip comments and trailing commas before sending it anywhere
npx strip-json-comments tsconfig.json | jq . > /tmp/clean.json

# JSON5 from the command line
npx json5 -s config.json5
# keep comments out of data files entirely
# config -> JSONC or YAML; payloads -> strict JSON, always

FAQ

Why does jq say 'Invalid numeric literal'?
The input is not JSON: it is probably HTML, a plain-text error page, or a response body that was already partially consumed. Check the content type and status code first, then look at the first few bytes with head -c 200.
Can I use comments in a JSON API body?
No. Comments are not part of the JSON grammar, and any compliant parser rejects them. If you want commented configuration, use JSONC, YAML or TOML for the file and convert to strict JSON before it is sent or stored as data.

Syntax and types When to use something else

Last refreshed 2026-09-18.