jsonpost
jqCLIJSON

A Practical jq Cookbook for Wrangling JSON

A hands-on jq cookbook of copy-paste recipes — pipes, map, select, building objects, group_by, to_entries, and using jq with curl on real API data.

JSONPost··5 min read
jq Cookbook

jq is a small, fast command-line tool for slicing, filtering, and reshaping JSON. Once it is in your muscle memory, you stop writing throwaway scripts and start transforming API responses with a single pipeline. This cookbook collects the recipes you will reach for most often, each with a sample input and the output it produces.

The sample data

Most recipes below operate on this users.json file. You can paste the same data into the jq playground to run any recipe without installing anything.

[
  { "id": 1, "name": "Ada",   "team": "platform", "active": true,  "logins": 42 },
  { "id": 2, "name": "Grace", "team": "platform", "active": false, "logins": 7  },
  { "id": 3, "name": "Linus", "team": "kernel",   "active": true,  "logins": 99 },
  { "id": 4, "name": "Margaret", "team": "kernel", "active": true, "logins": 12 }
]

The identity filter and pretty-printing

The simplest filter is ., the identity. It returns its input unchanged, which makes it a clean way to pretty-print and color any JSON.

echo '{"name":"Ada","logins":42}' | jq '.'
{
  "name": "Ada",
  "logins": 42
}

Accessing fields and the pipe

Use .field to reach into an object, and chain steps with the pipe |, exactly like a shell pipeline. Each stage feeds its output to the next.

echo '{"user":{"name":"Ada"}}' | jq '.user.name'
"Ada"

To drop the surrounding quotes and emit raw text, add the -r flag:

echo '{"user":{"name":"Ada"}}' | jq -r '.user.name'
Ada

Iterating an array with .[]

The array iterator .[] explodes an array into a stream of its elements. Each element then flows independently through the rest of the pipeline.

jq '.[].name' users.json
"Ada"
"Grace"
"Linus"
"Margaret"

You can index a single element with .[0] or take a slice with .[1:3].

Transforming with map

While .[] streams elements, map(f) applies a filter to every element and collects the results back into an array. This is the workhorse for reshaping lists.

jq 'map(.name)' users.json
["Ada", "Grace", "Linus", "Margaret"]

map can also build new shapes. Here we keep only the name and login count for each user:

jq 'map({ name, logins })' users.json
[
  { "name": "Ada", "logins": 42 },
  { "name": "Grace", "logins": 7 },
  { "name": "Linus", "logins": 99 },
  { "name": "Margaret", "logins": 12 }
]

Filtering with select

select(condition) passes through only the values for which the condition is true, and drops the rest. Combine it with the array iterator to filter a list. Here we keep only active users (the condition is .active == true):

jq '[ .[] | select(.active == true) ]'  users.json
[
  { "id": 1, "name": "Ada", "team": "platform", "active": true, "logins": 42 },
  { "id": 3, "name": "Linus", "team": "kernel", "active": true, "logins": 99 },
  { "id": 4, "name": "Margaret", "team": "kernel", "active": true, "logins": 12 }
]

Numeric filters work the same way. To list the names of users with more than 10 logins, where the condition is .logins > 10:

jq -r '.[] | select(.logins > 10) | .name'  users.json
Ada
Linus
Margaret

Building new objects

You construct objects with { } and arrays with [ ]. Reference incoming fields with the dot syntax and rename them freely. This recipe reshapes each user into a compact summary:

jq 'map({ user: .name, busy: (.logins > 20) })'  users.json
[
  { "user": "Ada", "busy": true },
  { "user": "Grace", "busy": false },
  { "user": "Linus", "busy": true },
  { "user": "Margaret", "busy": false }
]

Grouping with group_by

group_by(f) sorts the input by a key and then splits it into groups that share that key. The result is an array of arrays. Combine it with map to summarize each group — here we count users per team.

jq 'group_by(.team) | map({ team: .[0].team, count: length })'  users.json
[
  { "team": "kernel", "count": 2 },
  { "team": "platform", "count": 2 }
]

To total the logins per team instead, swap length for an add over the mapped logins:

jq 'group_by(.team) | map({ team: .[0].team, total: (map(.logins) | add) })'  users.json
[
  { "team": "kernel", "total": 111 },
  { "team": "platform", "total": 49 }
]

Converting objects with to_entries

to_entries turns an object into an array of key/value records, which is perfect for iterating over dynamic keys. Its inverse, from_entries, rebuilds an object. Given a settings object:

echo '{"dark":true,"fontSize":14}' | jq 'to_entries'
[
  { "key": "dark", "value": true },
  { "key": "fontSize", "value": 14 }
]

This pairs naturally with map to remap keys, then from_entries to collapse the result back into an object — a common pattern for renaming or filtering fields by name.

Combining filters

The real power of jq is composition. You can chain select, map, sorting, and slicing into one expression. Here we find active users, sort them by logins descending, and take the top two names:

jq -r '[ .[] | select(.active) ] | sort_by(-.logins) | .[0:2] | .[].name'  users.json
Linus
Ada

Using jq with curl

jq shines as the second half of a curl pipeline. Fetch JSON from an API and reshape it on the fly without ever saving a file. This example pulls open issues from a repository and prints just their titles:

curl -s https://api.example.com/repos/acme/app/issues \
  | jq -r '.[] | select(.state == "open") | .title'

The -s flag silences curl progress, and -r strips quotes so each title prints as plain text — ready to pipe into grep, sort, or a shell loop.

Conclusion

With a handful of building blocks — the pipe |, the iterator .[], map, select, object construction, group_by, and to_entries — you can answer almost any question about a JSON document in one line. The fastest way to internalize these is to run them against your own data and tweak until the output is exactly what you need.

Open the jq playground, paste the users.json sample from the top of this post, and work through each recipe yourself. Adjust the filters, break things, and watch how the output changes — that hands-on loop is what turns jq from intimidating to indispensable.

Keep reading