An API hands you 4,000 lines of JSON and you need three fields from it. Scrolling works once. The second time, reach for jq.

This jq cookbook collects the filters you’ll use most, grouped by task. Every example runs against the same small sample file, and every output shown came from running the command with jq 1.8.2. Copy a recipe, swap in your own field names, and move on.

What You’ll Learn

  • When to reach for jq, and when another tool fits better.
  • How jq filters and pipes work, so you can read any recipe.
  • How to pull values, filter arrays with select, and handle missing keys.
  • How to reshape objects, rename fields, and update values.
  • How to count, sum, group, and sort.
  • How to turn JSON into CSV, TSV, or plain text for other tools.
  • How to pass shell variables into jq and use jq in scripts.
  • How to apply all of it to real output from the GitHub API, kubectl, and the AWS CLI.

When to Use jq

jq earns its place when JSON stands between you and an answer. These use cases cover the jobs it handles best:

  • Read an API response. Pretty-print the payload, then pull the two or three fields you need out of a few thousand lines.
  • Filter a long array. Keep the records matching a condition and drop the rest, straight from the command line.
  • Reshape output for another tool. Rename fields, remove nulls, and hand a smaller object to the next command in the pipe.
  • Turn JSON into CSV or TSV. Move an API result into a spreadsheet, a database import, or anything that speaks columns.
  • Pull one value inside a shell script. Assign a token, an ID, or a count to a variable with -r, and retire the fragile grep and cut chain.
  • Query kubectl, the AWS CLI, and the GitHub API. All three emit JSON, so one set of filters works across them.
  • Build JSON to send. Construct a request body from shell variables with --arg and let jq handle the quoting.

Other tools fit better in a few places. Use grep when the text happens to be JSON but the question is a plain string match. Use a general-purpose language when the logic outgrows a one-liner, or when you need to join several documents. Reach for yq with YAML and dasel when the format varies.

Set Up jq and the Sample Data

Install jq with your package manager.

# macOS
brew install jq

# Debian or Ubuntu
sudo apt-get install jq

# Windows
winget install jqlang.jq

Check the version. The recipes below use features available in jq 1.7 and later.

jq --version

Create the sample file. Every recipe in this cookbook reads employees.json.

cat > employees.json <<'EOF'
{
  "company": "Acme",
  "employees": [
    { "id": 1, "name": "Ada Lopez", "email": "ada@acme.example", "dept": "Engineering", "age": 34, "salary": 145000, "skills": ["go", "sql"], "manager": null },
    { "id": 2, "name": "Ben Okafor", "email": "ben@acme.example", "dept": "Engineering", "age": 28, "salary": 118000, "skills": ["python"], "manager": 1 },
    { "id": 3, "name": "Chen Wu", "email": "chen@contractor.example", "dept": "Design", "age": 41, "salary": 99000, "skills": ["figma", "css"], "manager": 1 },
    { "id": 4, "name": "Dana Smith", "email": "dana@acme.example", "dept": "Sales", "age": 25, "salary": 72000, "skills": [], "manager": 3 }
  ]
}
EOF

Prefer to try the recipes without installing anything? The runner below holds the same employees.json and executes real jq 1.8.2 in your browser. Edit either box and press Run. Every filter in this cookbook works in it.

Press Run to execute this filter.

How jq Filters Work

A jq program is a filter. It takes JSON in, produces JSON out, and the pipe | passes one filter’s output to the next, the same way the shell pipes text between commands.

flowchart TB A["employees.json"] --> B[".employees[]
one object per employee"] B --> C["select(.age > 30)
keep matching objects"] C --> D[".name
take one field"] D --> E["Ada Lopez
Chen Wu"]

Four pieces of syntax cover most of what you’ll read:

  • . is the current input. .company reads a field from it.
  • .[] iterates an array, emitting each element separately.
  • | pipes each result into the next filter.
  • -r prints strings raw, without JSON quotes, and -c prints each result on one line.

jq Cookbook: Read Values

Pretty-Print JSON

Pass . to print the whole document, indented and colorized. This is the fastest way to make an unreadable API response readable.

jq . employees.json
curl --silent https://api.github.com/repos/jqlang/jq | jq .

Get a Field

Quote-free output matters when the result feeds another command, so add -r for strings.

jq '.company' employees.json
jq -r '.company' employees.json
"Acme"
Acme

Get a Value From Every Array Element

.employees[] emits each employee, and .name takes the name from each one.

jq -r '.employees[].name' employees.json
Ada Lopez
Ben Okafor
Chen Wu
Dana Smith

Index, Slice, and Count an Array

Arrays start at 0. Negative indexes count from the end, and slices include the start index but not the end index.

jq -c '.employees[0]' employees.json
jq -r '.employees[-1].name' employees.json
jq -c '.employees[1:3] | map(.name)' employees.json
jq '.employees | length' employees.json
{"id":1,"name":"Ada Lopez","email":"ada@acme.example","dept":"Engineering","age":34,"salary":145000,"skills":["go","sql"],"manager":null}
Dana Smith
["Ben Okafor","Chen Wu"]
4

Handle Missing Keys With a Default

jq returns null for a key that doesn’t exist instead of failing. The alternative operator // replaces null or false with a default.

jq '.employees[0].address.city' employees.json
jq '.employees[0].address.city // "unknown"' employees.json
null
"unknown"

Parse JSON From a URL

Pipe curl into jq. The --silent flag keeps curl’s progress meter out of the output.

curl --silent https://raw.githubusercontent.com/jeffabailey/learn/main/tools/jq/offices.json \
  | jq -r '.[] | "\(.name) is in \(.location)"'
Office 1 is in Oregon
Office 2 is in California
Office 3 is in Austria

Filter JSON With select

select(condition) passes its input through when the condition is true and drops it otherwise. Put it after .[] to filter an array element by element.

Filter by a Field Value

jq -c '.employees[] | select(.age > 30) | {name, age}' employees.json
jq -r '.employees[] | select(.dept == "Engineering") | .name' employees.json
{"name":"Ada Lopez","age":34}
{"name":"Chen Wu","age":41}
Ada Lopez
Ben Okafor

Combine Conditions With and, or, and not

jq -r '.employees[] | select(.dept == "Engineering" and .salary > 120000) | .name' employees.json
jq -r '.employees[] | select(.email | endswith("@acme.example") | not) | .email' employees.json
Ada Lopez
chen@contractor.example

Match Strings With a Regular Expression

test takes a regular expression and optional flags. The "i" flag ignores case. Escape a literal dot as \\. inside the jq string.

jq -r '.employees[] | select(.name | test("^[ab]"; "i")) | .name' employees.json
jq -r '.employees[] | select(.email | test("@acme\\.example$")) | .name' employees.json
Ada Lopez
Ben Okafor
Ada Lopez
Ben Okafor
Dana Smith

Filter on Array Contents

index("sql") returns the position of "sql" in the array, or null when it’s absent, so select keeps only employees who list the skill. Compare length to find empty arrays.

jq -r '.employees[] | select(.skills | index("sql")) | .name' employees.json
jq -r '.employees[] | select(.skills | length == 0) | .name' employees.json
Ada Lopez
Dana Smith

Find Null Values

jq -r '.employees[] | select(.manager == null) | .name' employees.json
Ada Lopez

Keep a Filtered Array Instead of a Stream

.[] | select(...) emits separate results. Wrap the filter in map to get one array back, which you can count or pass on.

jq '.employees | map(select(.dept == "Engineering")) | length' employees.json
2

Reshape JSON Objects

Pick Fields

{id, name} is shorthand for {id: .id, name: .name}.

jq -c '.employees | map({id, name})' employees.json
[{"id":1,"name":"Ada Lopez"},{"id":2,"name":"Ben Okafor"},{"id":3,"name":"Chen Wu"},{"id":4,"name":"Dana Smith"}]

Rename Fields

jq -c '.employees | map({employee_id: .id, full_name: .name})' employees.json
[{"employee_id":1,"full_name":"Ada Lopez"},{"employee_id":2,"full_name":"Ben Okafor"},{"employee_id":3,"full_name":"Chen Wu"},{"employee_id":4,"full_name":"Dana Smith"}]

Add and Delete Fields

+ merges objects, with the right side winning on conflicts. del removes one or more paths.

jq -c '.employees[0] | . + {remote: true}' employees.json
jq -c '.employees[0] | del(.skills, .manager)' employees.json
{"id":1,"name":"Ada Lopez","email":"ada@acme.example","dept":"Engineering","age":34,"salary":145000,"skills":["go","sql"],"manager":null,"remote":true}
{"id":1,"name":"Ada Lopez","email":"ada@acme.example","dept":"Engineering","age":34,"salary":145000}

Update a Value in Place

The update operator |= runs a filter on a value and writes the result back. This recipe gives everyone a 5% raise.

jq -c '.employees | map(.salary |= . * 1.05 | {name, salary})' employees.json
jq -c '.employees[] | .skills |= map(ascii_upcase) | {name, skills}' employees.json
[{"name":"Ada Lopez","salary":152250},{"name":"Ben Okafor","salary":123900},{"name":"Chen Wu","salary":103950},{"name":"Dana Smith","salary":75600}]
{"name":"Ada Lopez","skills":["GO","SQL"]}
{"name":"Ben Okafor","skills":["PYTHON"]}
{"name":"Chen Wu","skills":["FIGMA","CSS"]}
{"name":"Dana Smith","skills":[]}

Remove Null Fields

with_entries turns an object into {key, value} pairs, runs your filter on each pair, and rebuilds the object.

jq -c '.employees[0] | with_entries(select(.value != null))' employees.json
{"id":1,"name":"Ada Lopez","email":"ada@acme.example","dept":"Engineering","age":34,"salary":145000,"skills":["go","sql"]}

List the Keys of an Object

jq -c '.employees[0] | keys' employees.json
["age","dept","email","id","manager","name","salary","skills"]

Turn an Array Into a Lookup Object

Parentheses around a key evaluate it, so {(.name): .dept} uses each name as a key. add merges the resulting objects.

jq -c '.employees | map({(.name): .dept}) | add' employees.json
{"Ada Lopez":"Engineering","Ben Okafor":"Engineering","Chen Wu":"Design","Dana Smith":"Sales"}

Count, Sum, Group, and Sort

Sum and Average

jq '.employees | map(.salary) | add' employees.json
jq '.employees | map(.salary) | add / length' employees.json
434000
108500

Group and Count

group_by sorts by the key and returns an array of groups. Each group’s first element supplies the label.

jq -c '.employees | group_by(.dept) | map({dept: .[0].dept, count: length})' employees.json
[{"dept":"Design","count":1},{"dept":"Engineering","count":2},{"dept":"Sales","count":1}]

Sort and Find the Largest Value

sort_by sorts ascending. Add reverse for descending order, or use max_by and min_by when you only need one element.

jq -c '.employees | sort_by(.age) | map(.name)' employees.json
jq -c '.employees | sort_by(.age) | reverse | map(.name)' employees.json
jq -r '.employees | max_by(.age) | .name' employees.json
["Dana Smith","Ben Okafor","Ada Lopez","Chen Wu"]
["Chen Wu","Ada Lopez","Ben Okafor","Dana Smith"]
Chen Wu

Flatten and Deduplicate

[.employees[].skills[]] collects every skill into one array, and unique sorts it and drops duplicates.

jq -c '[.employees[].skills[]] | unique' employees.json
["css","figma","go","python","sql"]

Convert JSON to CSV, TSV, and Text

Convert JSON to CSV

Build an array per row, then format it with @csv. Use -r or the quotes get escaped a second time.

jq -r '.employees[] | [.id, .name, .dept] | @csv' employees.json
1,"Ada Lopez","Engineering"
2,"Ben Okafor","Engineering"
3,"Chen Wu","Design"
4,"Dana Smith","Sales"

Add a header row by emitting it first.

jq -r '["id","name","dept"], (.employees[] | [.id, .name, .dept]) | @csv' employees.json

Convert JSON to TSV

@tsv is easier to feed into cut, awk, or a while read loop because fields never contain unescaped tabs.

jq -r '.employees[] | [.name, .email] | @tsv' employees.json
Ada Lopez	ada@acme.example
Ben Okafor	ben@acme.example
Chen Wu	chen@contractor.example
Dana Smith	dana@acme.example

Build Strings With Interpolation

\(...) inside a string inserts the result of a filter.

jq -r '.employees[] | "\(.name) works in \(.dept)"' employees.json
Ada Lopez works in Engineering
Ben Okafor works in Engineering
Chen Wu works in Design
Dana Smith works in Sales

Use jq in Shell Scripts

Pass Shell Variables Into jq

Never paste a shell variable into the filter string. Quoting breaks the first time a value contains a quote. Pass strings with --arg and numbers or JSON with --argjson.

jq -r --arg dept Sales '.employees[] | select(.dept == $dept) | .name' employees.json
jq -r --argjson min 100000 '.employees[] | select(.salary >= $min) | .name' employees.json
Dana Smith
Ada Lopez
Ben Okafor

Build JSON From Scratch

-n starts with no input, which makes jq a safe JSON generator for request bodies.

jq -n --arg name "Eve Park" --argjson age 30 '{name: $name, age: $age}'
{
  "name": "Eve Park",
  "age": 30
}

Branch on a Result With the Exit Status

-e sets the exit status from the last output: 1 when it’s false or null, 0 otherwise. Use it in if statements instead of comparing strings.

if jq -e '.employees | any(.dept == "Legal")' employees.json > /dev/null; then
  echo "Legal has staff"
else
  echo "No one in Legal"
fi
No one in Legal

Edit a JSON File in Place

jq has no in-place flag. Write to a temporary file, then move it over the original, so a failed filter can’t truncate your file.

tmp="$(mktemp)" && jq '.company = "Acme Corp"' employees.json > "$tmp" && mv "$tmp" employees.json

Search and Combine JSON

Find a Key Anywhere in a Document

.. walks every value recursively. .email? reads the key where it exists, and // empty drops the misses.

jq -r '[.. | .email? // empty] | join(", ")' employees.json
ada@acme.example, ben@acme.example, chen@contractor.example, dana@acme.example

List the Paths to Matching Values

paths(f) returns the path to every value where f is true, which helps you learn the shape of an unfamiliar document.

jq -c '[paths(type == "number")] | .[:3]' employees.json
[["employees",0,"id"],["employees",0,"age"],["employees",0,"salary"]]

Combine Several Files

-s (slurp) reads every input into one array. * deep-merges two objects.

jq -s 'map(.employees | length) | add' employees.json employees.json
jq -c '.employees[0] * {skills: ["go", "sql", "rust"]} | {name, skills}' employees.json
8
{"name":"Ada Lopez","skills":["go","sql","rust"]}

Real-World jq Recipes

The same filters work on JSON from the tools you already run. The kubectl and AWS CLI outputs below come from sample responses in the same shape those commands return.

Query the GitHub API

curl --silent https://api.github.com/repos/jqlang/jq \
  | jq '{name: .full_name, stars: .stargazers_count}'

curl --silent "https://api.github.com/repos/jqlang/jq/releases?per_page=3" \
  | jq -r '.[] | "\(.tag_name)\t\(.published_at[:10])"'
jq-1.8.2	2026-06-20
jq-1.8.1	2025-07-01
jq-1.8.0	2025-06-01

List Kubernetes Pods That Aren’t Running

kubectl get pods -o json \
  | jq -r '.items[] | select(.status.phase != "Running") | "\(.metadata.name)\t\(.status.phase)"'
worker-5c2a	Pending
cron-1b3e	Failed

List EC2 Instances With Their Name Tags

AWS returns tags as a list of Key and Value pairs, so select the pair whose key is Name.

aws ec2 describe-instances \
  | jq -r '.Reservations[].Instances[] | [.InstanceId, .State.Name, (.Tags[]? | select(.Key == "Name") | .Value)] | @tsv'
i-0abc	running	api
i-0def	stopped	batch

jq Cheat Sheet

jq .                                  # pretty-print
jq -r '.key'                          # raw string, no quotes
jq -c '.'                             # one result per line
jq '.a.b.c'                           # nested field
jq '.a // "default"'                  # default for null or missing
jq '.[]'                              # iterate an array
jq '.[0]' / '.[-1]' / '.[2:5]'        # index, last element, slice
jq 'length'                           # array, object, or string length
jq '.[] | select(.n > 1)'             # filter elements
jq 'map(select(.ok))'                 # filter, keep an array
jq 'map({id, name})'                  # pick fields
jq '. + {k: 1}'                       # add or overwrite a field
jq 'del(.k)'                          # delete a field
jq '.n |= . + 1'                      # update in place
jq 'with_entries(select(.value))'     # filter object entries
jq 'map(.n) | add'                    # sum
jq 'group_by(.k)'                     # group
jq 'sort_by(.k) | reverse'            # sort descending
jq 'unique'                           # sort and deduplicate
jq -r '.[] | [.a, .b] | @csv'         # CSV
jq -r '.[] | [.a, .b] | @tsv'         # TSV
jq -r '"\(.a) and \(.b)"'             # string interpolation
jq --arg v "$VAR" '.k == $v'          # pass a string
jq --argjson n 5 '.k > $n'            # pass a number or JSON
jq -n '{k: 1}'                        # build JSON from nothing
jq -e '.ok'                           # exit status from the result
jq -s '.'                             # slurp inputs into one array
jq '.. | .k? // empty'                # find a key anywhere

Learn jq: Beyond the Basics

When a recipe here doesn’t fit, these resources go deeper.

  • jq Manual: the full reference for every built-in function, operator, and flag.
  • jq Tutorial: the official walkthrough using live GitHub API data.
  • jq Wiki Cookbook: community recipes for harder problems like recursive updates and streaming.
  • jq Playground: paste JSON and a filter in the browser and see the output as you type.
  • Online Generate Test Data: generate larger JSON files to practice on.