Writing jq Filters With LLMs in October 2026: Paste Real Sample Input, Handle Nulls on Purpose, and Test Before You Pipe
jq is one of those tools almost everyone uses and almost nobody remembers. You need to pull three fields out of an API response, flatten a nested array into CSV, or count records by status, and you end up asking an LLM for the filter. That works well most of the time. The failures are quiet: a filter that returns nothing instead of an error, a default that swallows a real false, or a shell command that breaks the moment a value contains a space. This guide covers how to ask for jq filters so the first answer is usually right, and how to check the ones that aren't.
Example JSON code. Image: Dcbmariano, CC BY 4.0, via Wikimedia Commons.
Why LLM-written jq filters go wrong
jq looks simple, but its behavior around missing data is subtle, and models tend to write for the happy path. The common problems are:
- Guessed structure. If you describe your JSON in words instead of pasting it, the model invents field names and nesting. The filter then runs without error and prints
nullover and over. - Null handling by accident. In jq,
.user.nameon an object with nouserkey returnsnullquietly, but.items[]on a missingitemsstops with "Cannot iterate over null". Generated filters often get one case right and not the other. - Version and implementation drift. Functions like
pickarrived in jq 1.7. Older distro packages, and alternative implementations such as gojq and jaq, differ in some functions and edge cases. A filter that works in the model's head may not work on your machine. - Shell quoting. Models sometimes splice shell variables straight into the filter string, which breaks on quotes and spaces and can change what the filter does.
Step 1: Give the model real sample input
The single best thing you can do is paste a small, real sample of the JSON, trimmed down but with the actual shape. Include at least one "awkward" record: a missing field, an empty array, a null, a boolean that is false. If the data is sensitive, replace values but keep keys, types, and nesting exactly.
A good request looks like this:
I'm using jq 1.7.1 (output of jq --version) in bash.
Input is a JSON array like this (2 of ~5,000 records):
[
{"id": 101, "status": "active", "owner": {"email": "a@example.com"}, "tags": ["beta"], "archived": false},
{"id": 102, "status": "disabled", "owner": null, "tags": [], "archived": true}
]
I want tab-separated output: id, status, owner email (or "-" if missing),
and the tags joined with commas. Skip archived records.
Explain what happens for record 102.
Naming the version and the shell matters. Asking the model to explain what happens to the awkward record forces it to reason about nulls instead of skipping over them.
Step 2: Know the null and default traps
You'll review generated filters faster if you know where jq tends to surprise people.
The alternative operator treats false like null
a // b returns b when a produces null or false (or nothing). So .archived // true turns every real false into true. For booleans, test for the key explicitly instead:
if has("archived") then .archived else true end
Iterating over something that might be missing
.tags[] errors when tags is missing or null. Either use the optional form .tags[]?, which produces nothing instead of an error, or supply a default first: (.tags // [])[]. Be deliberate about which you want. Suppressing errors with ? everywhere hides real schema changes.
select keeps or drops whole values
select(.status == "active") passes the input through when the condition is true and outputs nothing otherwise. A filter that ends with an empty result often has a select comparing against the wrong type, such as the string "101" instead of the number 101.
Here is a filter for the request above that handles each of these cases on purpose:
jq -r '
.[]
| select(.archived != true)
| [ .id,
.status,
(.owner.email // "-"),
((.tags // []) | join(","))
]
| @tsv
' records.json
Note that .owner.email is safe even when owner is null, because indexing null with a key returns null. And @tsv and @csv need an array as input and -r to print raw text instead of a quoted JSON string.
Step 3: Pass shell values with --arg, never string splicing
If a filter needs a value from your script, ask for --arg or --argjson explicitly. Reject anything that builds the filter with double quotes and $VAR inside it.
# Fragile: breaks on quotes and spaces, and the value becomes jq code
jq ".[] | select(.status == \"$STATUS\")" records.json
# Safe: the value is passed as data
jq --arg status "$STATUS" '.[] | select(.status == $status)' records.json
# Numbers need --argjson (or tonumber), because --arg always passes a string
jq --argjson min "$MIN_ID" '.[] | select(.id >= $min)' records.json
The string-versus-number mismatch with --arg is a classic silent failure: select(.id == $id) matches nothing because 101 is not equal to "101".
Pipes and valves on an industrial machine. Image: Shixart1985, CC BY 2.0, via Wikimedia Commons.
Step 4: Test against fixtures before you pipe anything
Treat a jq filter you'll reuse like a small piece of code. Keep it in a file, keep sample inputs next to it, and check the output.
- Save the filter as
report.jqand run it withjq -r -f report.jq. Files avoid quoting problems entirely. - Save two or three fixture files: a normal record set, one with missing and
nullfields, and an empty array. - Save the expected output for each, then compare:
jq -r -f report.jq fixtures/missing.json | diff - fixtures/missing.expected.tsv. - Run against the empty input and make sure you get empty output, not an error.
- Only then run it on the real data, and spot-check a few lines against the source.
When you ask the model to fix a failing filter, paste the exact error and the input that caused it. "It doesn't work" leads to guesses; "Cannot iterate over null (null) on this record" leads to a precise fix.
Step 5: Make scripts fail loudly
By default, jq exits successfully even when the result is null. In scripts, that means a bad field name can quietly produce empty config or empty reports. The -e flag changes this: jq exits with status 1 if the last output was false or null, and 4 if it produced no valid result.
set -euo pipefail
token=$(jq -er '.auth.token' response.json) || {
echo "no token in response" >&2
exit 1
}
Ask the model to use -e for any lookup your script depends on, and to explain which exit codes it relies on.
Useful patterns to ask for by name
Models do better when you name the jq building blocks you expect. These cover most day-to-day jobs:
- Reshape objects:
{id, email: .owner.email}builds a smaller object;pick(.id, .owner.email)keeps the original nesting (jq 1.7 or later). - Count and group:
group_by(.status) | map({status: .[0].status, count: length}). - Combine many documents:
jq -s(slurp) reads a stream of JSON values into one array, which helps with newline-delimited logs. - Work with keys:
to_entries,from_entries, andwith_entrieslet you rename or filter keys in a map. - Compact output for line tools:
jq -c '.[]'prints one JSON object per line, ready forgrep,wc -l, or another jq. - Find where a field lives:
[paths | map(tostring) | join(".")]lists paths in an unfamiliar document so you can paste the right ones back into your prompt.
For very large files, mention the size. Slurping a multi-gigabyte array into memory is a different problem from filtering a 50 KB API response, and jq's --stream mode exists for the first case, though its filters are harder to read and worth testing carefully.
A short checklist
- Paste a real, trimmed sample with at least one awkward record.
- State your jq version (
jq --version) and shell. - Check every
//default: could the real value befalse? - Check every
[]: what happens if the array is missing ornull? - Pass values with
--argor--argjson, not string splicing. - Keep the filter in a file and diff it against fixture output.
- Use
-ein scripts so a missing value stops the run.
jq rewards the same habit as every other tool an LLM writes for you: show it real input, ask it to explain the edge cases, and run the result on data you already understand before trusting it with data you don't.
Comments
Post a Comment