Querying JSON with jq and JSONPath

Intermediate
13 min

Querying JSON with jq and JSONPath

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.

Installing jq

jq is a single binary in every package manager:

bash
brew install jq # macOS sudo apt install jq # Debian and Ubuntu winget install jqlang.jq # Windows jq --version # jq-1.7.1 or newer

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

jq Filters: Paths, Iteration and Pipes

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:

bash
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.333333333333336

select(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.

Combining jq With Other Tools

jq shines in pipelines, turning API responses into shell variables, CSV reports and CI checks:

bash
# 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: Queries Inside Programs

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:

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

Tips

  • Quote jq filters with single quotes so the shell does not interpret $, | or >.
  • Use --arg and --argjson rather than string interpolation to inject values; it prevents quoting bugs.
  • Never extract values from JSON with grep or cut; they break on reformatting, escaping and nesting.
  • AWS CLI and Azure CLI use JMESPath (--query), a third syntax with similar ideas; know which one a tool expects.
Quick Quiz
Question 1 of 3

What does `jq '.users[] | select(.age > 30) | .name' users.json` output?

Key Takeaways

  • 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.
  • Pass shell values with --arg, and never parse JSON with grep.
  • JSONPath (RFC 9535) expresses queries as strings such as $.users[?@.age > 30].name and is used by kubectl, Postman and query libraries.
  • JSON Pointer addresses a single value (/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.

Querying JSON with jq and JSONPath - JSON | CodeYourCraft | CodeYourCraft