jq — JSON processing with the jq CLI
When to use
- User wants to extract, filter, transform, aggregate, or pretty-print JSON with
jq
- Input is JSON / JSON array / NDJSON (newline-delimited JSON), API payloads, logs, configs
- Prefer this over writing a throwaway Python/Node script for the same JSON job
Do not use when:
- File is binary, CSV, XML, YAML-only (unless already converted to JSON)
- Task is general bash/Python scripting with no JSON core
- User only needs to open/edit JSON in an editor (no filter)
Prerequisites
command -v jq >/dev/null || { echo "jq not installed"; exit 1; }
jq --version # expect 1.6+
If missing: tell the user to install (brew install jq / apt install jq) and STOP.
Procedure
Work phases in order. Do not skip. Prefer pure jq over python/node for JSON work.
Phase 1 — Structure analysis
- Identify inputs: path(s), stdin, or API response the user provided.
- Peek schema before complex filters:
- Small file:
jq 'type, (if type=="array" then length else keys end)' <file>
- Huge / unknown:
jq -c 'limit(1; .)' <file> or first NDJSON line via head -n 1
- Validate:
jq empty <file> — non-zero exit → report parse error and STOP
- Note shape: object vs array vs NDJSON stream; nested keys needed; size class (<10MB / large).
- Completion: input path(s) known, type known, filter target keys identified (or error reported).
Phase 2 — Filter construction
Design filter with explicit pipeline stages (compose with |):
- Select path into focus:
.items[], .[], .data.results?
- Filter rows:
select(.status == "active")
- Transform shape:
{id, name: .user.name} or map(...)
- Aggregate if needed:
group_by(.k) | map({k: .[0].k, n: length}) or map(.items | map(.price*.qty) | add)
- Output flags: pretty default;
-r bare strings; -c compact; -s only if slurp is required
Safety (mandatory):
- Pass untrusted strings via
--arg / --argjson, never interpolate into the filter string
- Optional paths: use
? (.a.b?) to avoid hard errors on missing keys
- NEVER redirect jq onto the same path it reads:
jq ... file > file truncates the file. Always:
jq '<filter>' file.json > file.json.tmp && mv file.json.tmp file.json
Deep cookbook (load only if needed): references/patterns.md
- Completion: one copy-pastable
jq ... command ready; --arg used where user/env text is involved.
Phase 3 — Execution
- Run via shell:
jq [flags] '<filter>' <file> or pipe into jq.
- File rewrites: temp +
mv only (see Safety).
- Large files / NDJSON: stream; do not
jq -s for simple filters/counts.
- On non-zero exit: capture stderr; fix or report; do not invent output.
- Always include the exact command in the user-facing answer.
- Completion: command ran; stdout/stderr captured; exit code known.
Phase 4 — Validation
- Empty result → re-check with
keys, sample object, ? / casing.
- Confirm output type matches request (JSON vs raw list vs count).
- For file writes:
jq empty file.json and spot-check changed + preserved fields.
- Return command + short summary + truncated sample if large.
- Completion: answer matches intent, or clear failure + diagnostic.
Essential patterns (keep loaded)
jq '.users[] | select(.age > 21)' data.json
jq 'map({name: .user.name, role: .auth.role})' data.json
jq 'group_by(.category) | map({cat: .[0].category, count: length})' data.json
jq --arg id "$ID" '.[] | select(.id == $id)' data.json
jq -r '.[].title' pulls.json
jq -c 'select(.level=="error" and .service=="auth")' app.ndjson | wc -l
jq '.version="2.0.0"' package.json > package.json.tmp && mv package.json.tmp package.json
# nested totals example
jq '[.orders[] | select(.status=="paid") | {buyer: .buyer.name, total: ([.items[] | .price*.qty] | add)}] | sort_by(-.total)' orders.json
Guardrails
- Validate JSON first (
jq empty).
--arg / --argjson for external values; single-quote filters in the shell.
- Avoid
-s on large/NDJSON inputs for simple work.
- No shell-injecting user text into the filter body.
- Prefer
jq over Python/Node for the same JSON transform.
- File rewrite: temp +
mv only; never jq ... f > f.
Examples
Extract names (raw)
jq -r '.users[] | select(.status=="active" and .age>21) | .name' users.json | sort
Update config safely
jq '.version="2.0.0" | .scripts.build="tsc -b"' package.json > package.json.tmp && mv package.json.tmp package.json
Env-safe lookup
jq --arg name "$TARGET_NAME" '.users[] | select(.name == $name)' users.json
Non-trigger
"Convert this CSV" / "write a Python ETL" → do not load this skill.
References (load only if needed)
references/patterns.md — streaming, merge, walk, try/catch, group_by detail
1---2name: jq3description: Trigger on: jq, jq filter, jq query, process JSON, filter JSON, transform JSON, extract from JSON, parse API response JSON, NDJSON, pretty-print JSON, jq select, group_by JSON, update JSON file with jq. Specialized procedure for complex JSON processing with the jq CLI. Prefer over ad-hoc Python/Node one-offs for extract, filter, format, aggregate, or in-place JSON transforms. Do not use for binary files, CSV/XML conversion, or general scripting unrelated to JSON.4license: MIT5---67# jq — JSON processing with the jq CLI89## When to use1011- User wants to extract, filter, transform, aggregate, or pretty-print JSON with `jq`12- Input is JSON / JSON array / NDJSON (newline-delimited JSON), API payloads, logs, configs13- Prefer this over writing a throwaway Python/Node script for the same JSON job1415Do **not** use when:1617- File is binary, CSV, XML, YAML-only (unless already converted to JSON)18- Task is general bash/Python scripting with no JSON core19- User only needs to open/edit JSON in an editor (no filter)2021## Prerequisites2223```bash24command -v jq >/dev/null || { echo "jq not installed"; exit 1; }25jq --version # expect 1.6+26```2728If missing: tell the user to install (`brew install jq` / `apt install jq`) and STOP.2930## Procedure3132Work phases in order. Do not skip. Prefer pure `jq` over `python`/`node` for JSON work.3334### Phase 1 — Structure analysis35361. Identify inputs: path(s), stdin, or API response the user provided.372. Peek schema before complex filters:38 - Small file: `jq 'type, (if type=="array" then length else keys end)' <file>`39 - Huge / unknown: `jq -c 'limit(1; .)' <file>` or first NDJSON line via `head -n 1`40 - Validate: `jq empty <file>` — non-zero exit → report parse error and STOP413. Note shape: object vs array vs NDJSON stream; nested keys needed; size class (<10MB / large).424. **Completion:** input path(s) known, type known, filter target keys identified (or error reported).4344### Phase 2 — Filter construction4546Design filter with explicit pipeline stages (compose with `|`):47481. **Select** path into focus: `.items[]`, `.[]`, `.data.results?`492. **Filter** rows: `select(.status == "active")`503. **Transform** shape: `{id, name: .user.name}` or `map(...)`514. **Aggregate** if needed: `group_by(.k) | map({k: .[0].k, n: length})` or `map(.items | map(.price*.qty) | add)`525. **Output flags:** pretty default; `-r` bare strings; `-c` compact; `-s` only if slurp is required5354**Safety (mandatory):**5556- Pass untrusted strings via `--arg` / `--argjson`, never interpolate into the filter string57- Optional paths: use `?` (`.a.b?`) to avoid hard errors on missing keys58- NEVER redirect jq onto the same path it reads: `jq ... file > file` truncates the file. Always:5960```bash61jq '<filter>' file.json > file.json.tmp && mv file.json.tmp file.json62```6364Deep cookbook (load only if needed): `references/patterns.md`65666. **Completion:** one copy-pastable `jq ...` command ready; `--arg` used where user/env text is involved.6768### Phase 3 — Execution69701. Run via shell: `jq [flags] '<filter>' <file>` or pipe into `jq`.712. File rewrites: temp + `mv` only (see Safety).723. Large files / NDJSON: stream; do **not** `jq -s` for simple filters/counts.734. On non-zero exit: capture stderr; fix or report; do not invent output.745. Always include the exact command in the user-facing answer.756. **Completion:** command ran; stdout/stderr captured; exit code known.7677### Phase 4 — Validation78791. Empty result → re-check with `keys`, sample object, `?` / casing.802. Confirm output type matches request (JSON vs raw list vs count).813. For file writes: `jq empty file.json` and spot-check changed + preserved fields.824. Return command + short summary + truncated sample if large.835. **Completion:** answer matches intent, or clear failure + diagnostic.8485## Essential patterns (keep loaded)8687```bash88jq '.users[] | select(.age > 21)' data.json89jq 'map({name: .user.name, role: .auth.role})' data.json90jq 'group_by(.category) | map({cat: .[0].category, count: length})' data.json91jq --arg id "$ID" '.[] | select(.id == $id)' data.json92jq -r '.[].title' pulls.json93jq -c 'select(.level=="error" and .service=="auth")' app.ndjson | wc -l94jq '.version="2.0.0"' package.json > package.json.tmp && mv package.json.tmp package.json95# nested totals example96jq '[.orders[] | select(.status=="paid") | {buyer: .buyer.name, total: ([.items[] | .price*.qty] | add)}] | sort_by(-.total)' orders.json97```9899## Guardrails100101- Validate JSON first (`jq empty`).102- `--arg` / `--argjson` for external values; single-quote filters in the shell.103- Avoid `-s` on large/NDJSON inputs for simple work.104- No shell-injecting user text into the filter body.105- Prefer `jq` over Python/Node for the same JSON transform.106- File rewrite: temp + `mv` only; never `jq ... f > f`.107108## Examples109110### Extract names (raw)111112```bash113jq -r '.users[] | select(.status=="active" and .age>21) | .name' users.json | sort114```115116### Update config safely117118```bash119jq '.version="2.0.0" | .scripts.build="tsc -b"' package.json > package.json.tmp && mv package.json.tmp package.json120```121122### Env-safe lookup123124```bash125jq --arg name "$TARGET_NAME" '.users[] | select(.name == $name)' users.json126```127128### Non-trigger129130"Convert this CSV" / "write a Python ETL" → do not load this skill.131132## References (load only if needed)133134- `references/patterns.md` — streaming, merge, walk, try/catch, group_by detail