Once a JSON document is bigger than a screen, you need to ask it questions: which users are admins, what is the third order's total, which email addresses appear anywhere in the tree. This lesson teaches the two standard query languages for JSON: jq, the command-line processor every developer should know, and JSONPath, the expression syntax used inside programs and tools such as Kubernetes and Postman.
jq is a single binary in every package manager:
brew install jq # macOS
sudo apt install jq # Debian and Ubuntu
winget install jqlang.jq # Windows
jq --version # jq-1.7.1 or newerIt reads JSON from a file or standard input, applies a filter, and prints the result pretty-printed. The identity filter . just formats the input, which is why curl -s url | jq . is the usual way to inspect an API response.
Paths look like JavaScript property access, [] without an index iterates an array, and | pipes the output of one filter into the next, as in a shell. Using the users.json document from the sample above:
jq '.users[0].name' users.json # "Ada"
jq -r '.users[].email' users.json # ada@example.com, linus@example.com, null
jq '.users | length' users.json # 3
jq '.users[] | select(.age > 30) | .name' users.json # "Ada" "Grace"
jq '.users | map(.age) | add / length' users.json # 36.333333333333336select(condition) keeps matching inputs, map(f) applies f to every element, and -r prints strings without quotes for use in shell pipelines. A few more filters cover most daily work:
| Filter | Purpose |
|--------|---------|
| keys, has("k"), del(.k) | Inspect or remove object keys |
| sort_by(.age), group_by(.team), unique | Reorder and group arrays |
| {name, email} | Build a new object from selected fields |
| to_entries, from_entries | Convert between objects and key/value arrays |
| @csv, @tsv, @base64 | Format an array as CSV, TSV or Base64 |
| --arg name value, $name | Pass shell values in safely |
| -c, -s, -e | Compact output, slurp inputs into one array, exit status reflects the result |
Filters also transform documents: jq '.users[0].age = 37' updates a value and jq 'del(.users[].email)' strips a field everywhere. Redirect output to a new file; jq never edits in place.
jq shines in pipelines, turning API responses into shell variables, CSV reports and CI checks:
# Emails of active users from a paginated API
curl -s "https://api.example.com/users?page=1" | jq -r '.data[] | select(.active) | .email'
# Count users per role, as a CSV report
jq -r '[.users[] | .roles[]] | group_by(.) | map([.[0], length]) | .[] | @csv' users.json
# "admin",1
# "dev",2
# "ops",1
# Fail a CI step when a field is missing (-e sets the exit code)
jq -e '.version' package.json > /dev/null || echo "version missing"JSONPath is to JSON what XPath is to XML: a compact expression that selects nodes, standardized in 2024 as RFC 9535 after years of dialects. The root is $, .name and [0] descend, [*] selects all elements, .. searches recursively, [?...] filters and [start:end] slices:
| Expression | Selects |
|------------|---------|
| $.users[*].name | Every user's name |
| $.users[?@.age > 30].name | Names of users older than 30 |
| $..email | Every email anywhere in the document |
| $.users[-1] | The last user |
| $.users[0:2] | The first two users |
Older implementations write filters as [?(@.age > 30)] with the same meaning. JSONPath appears wherever a tool needs a query as a string: kubectl -o jsonpath='{.items[*].metadata.name}', Postman assertions, Grafana dashboards. In code, use jsonpath-plus for JavaScript or jsonpath-ng for Python:
import { JSONPath } from "jsonpath-plus";
const seniors = JSONPath({ path: "$.users[?(@.age > 30)].name", json: data });
console.log(seniors); // ["Ada", "Grace"]A simpler relative is JSON Pointer (RFC 6901): a path such as /users/0/name that addresses exactly one value, with no wildcards or filters; JSON Schema $ref and JSON Patch use it.
$, | or >.--arg and --argjson rather than string interpolation to inject values; it prevents quoting bugs.grep or cut; they break on reformatting, escaping and nesting.--query), a third syntax with similar ideas; know which one a tool expects.What does `jq '.users[] | select(.age > 30) | .name' users.json` output?
jq reads JSON, applies a filter, and prints the result; . formats, .a.b[0] navigates, [] iterates and | chains.select, map, sort_by, group_by and @csv turn documents into reports; -r, -c and -e shape the output for scripts.--arg, and never parse JSON with grep.$.users[?@.age > 30].name and is used by kubectl, Postman and query libraries./users/0/name) and underpins JSON Schema and JSON Patch.Next lesson: JSON Lines and Streaming Large JSON Files — process gigabytes of JSON without loading it all into memory.