jq を使いこなすための実践パターン — JSON を秒で料理する日常コマンド集
深夜2時、Raspberry Pi 5 から叩いた証券会社の WebSocket API が 8KB くらいの JSON を吐いて、cat で開いたら端末が真っ黒になった。python3 -m json.tool でパイプして人間に読める形にして、該当フィールドを grep で探して… それを 10 回くらい繰り返したところで、そろそろ jq を真面目に覚えようと決めた。翌朝、同じ作業が 3 秒で終わった。
知っているコマンドと、実戦で使えるコマンドは違う。jq '.' で整形できることは全員が知っている。でも select と map と --arg を組み合わせて 1 行でフィルタを書けるかは、別の話だ。この記事は、自分が毎日叩いている jq のパターンを、使用頻度の高い順に並べたメモだ。
インストールはapt install jqかbrew install jq。バージョンは 1.6 以降を想定(1.7 が出てから一部の挙動が変わっている)。
grep で JSON を触るのはもう止めよう
grep で JSON を漁るクセが抜けない人、わりといる。自分もそうだった。一行フラットな JSON なら動いてしまうし、| jq を付け忘れたまま検索が通ることもある。だが、ネストした構造のキーを拾いたい瞬間に破綻する。
例えば、こういう API レスポンスから status == "filled" のオーダーだけ抜きたいとする。
{
"orders": [
{"id": "A-001", "status": "filled", "price": 1240},
{"id": "A-002", "status": "pending", "price": 1250},
{"id": "A-003", "status": "filled", "price": 1238}
]
}
grep だと「この行」しか見えない。jq なら構造を理解した上で叩ける。
jq '.orders[] | select(.status == "filled")' response.json
以前書いたripgrep の実践テクニックと同じ思想で、「構造を理解したツールを使え」というだけの話なのだが、jq は文法が独特なので一度真面目に座って覚える必要がある。覚える価値はある。
覚えると効く基本フィルタ
整形とキー確認
curl -s https://api.example.com/orders | jq '.'
curl -s https://api.example.com/orders | jq 'keys'
curl -s https://api.example.com/orders | jq 'keys_unsorted'
keys はアルファベット順で返す。挿入順を保ちたいなら keys_unsorted。見落としがちだが、後者のほうが「この API はどういう構造か」を読むときに便利だ。
ネストされた値を取り出す
jq '.user.profile.email' user.json
jq '.user.profile.email // "unknown"' user.json
// はデフォルト値演算子。キーが存在しないときの null を別の値で置き換えたいときに使う。シェルで | xargs に渡すときに null を通してしまうと事故るので、このクセは早めに付けておくといい。
配列の展開と再収集
jq '.orders[]' response.json # 1 要素ずつ出力
jq '[.orders[] | .id]' response.json # id だけ配列で取り直す
jq '.orders | length' response.json # 要素数
.orders[] と .orders は別物。前者はストリーム化(オブジェクトが連続で出る)、後者は配列のまま。頭の中で「配列かストリームか」を意識しておかないと、後段のフィルタで詰まる。
select で条件フィルタ
jq '.orders[] | select(.price > 1239)' response.json
jq '.orders[] | select(.status == "filled" and .price < 1240)' response.json
SQL の WHERE 相当。select は真を返す要素をそのまま流し、偽のものは消す。and / or / not で組み合わせられる。
map と reduce で集計する
配列を別の配列に写像したいとき、map を使う。
jq '.orders | map(.price)' response.json
# → [1240, 1250, 1238]
jq '.orders | map(select(.status == "filled") | .price) | add' response.json
# → 2478 (filled だけの合計)
jq '.orders | map(.price) | add / length' response.json
# → 1242.67 (平均)
add は数値の合計、文字列の連結、配列の flatten まで面倒を見る万能選手だ。length と組み合わせれば平均も 1 行。
もう一歩踏み込むなら reduce。
jq '.orders | reduce .[] as $o (0; . + $o.price)' response.json
慣れないと読みにくいが、初期値とアキュムレータの更新式を書くだけ。「状態を持ったループ」が必要になったら思い出すといい。
シェル変数を安全に渡す
jq のフィルタ内でシェル変数を使うとき、絶対に \"$VAR\" で埋め込んではいけない。値に引用符や特殊文字が混ざると、フィルタが壊れる。かわりに --arg と --argjson を使う。
KEYWORD="A-001"
jq --arg kw "$KEYWORD" '.orders[] | select(.id == $kw)' response.json
THRESHOLD=1240
jq --argjson t "$THRESHOLD" '.orders[] | select(.price > $t)' response.json
--arg は値を常に文字列として渡し、--argjson は JSON としてパースする(数値や真偽値はこっち)。シェルスクリプトから jq を呼ぶときは、これを徹底するだけで事故が激減する。自動化スクリプトの中で jq を使うパターンは、Claude Code と cron の組み合わせの記事でも書いたような定期ジョブで特に頻出する。
curl と組み合わせる現場パターン
単純な API 叩きとフィールド抽出
curl -s https://api.github.com/repos/stedolan/jq \
| jq '{name, stars: .stargazers_count, updated: .updated_at}'
返ってきた巨大な JSON から、必要な 3 つだけ新しいオブジェクトに組み直す。これが一番使う。
複数リクエストを投げて集計する
for repo in stedolan/jq BurntSushi/ripgrep; do
curl -s "https://api.github.com/repos/$repo" \
| jq --arg r "$repo" '{repo: $r, stars: .stargazers_count}'
done | jq -s 'sort_by(-.stars)'
jq -s(slurp)は、複数の JSON を 1 つの配列にまとめてから処理する。ループの出力をパイプで一気に受けて、集計・並べ替えまで済ませられる。
エラー時に終了コードを伝える
if ! curl -s https://api.example.com/health | jq -e '.status == "ok"'; then
echo "health check failed" >&2
exit 1
fi
-e は、フィルタの結果が false/null だと終了コード 1 を返すオプション。シェルスクリプトの条件分岐に組み込むときに必須。これを知らずに grep で代用していた時期があって、後でログをjournalctl で追うときに「何が失敗したのか」が追えなくなって痛い目を見た。
ハマりどころと救済策
巨大な JSON でメモリが飛ぶ
数百 MB の JSON を jq に流すと、Raspberry Pi だとメモリ不足で落ちる。自分は一度これで SD カードを道連れにしかけた。救いは --stream モードだ。
jq --stream 'select(.[0][0] == "orders") | .[1]' huge.json
構造全体をメモリに乗せず、パス+値のペアを順に流す。書き方にクセがあるので、最初は 公式マニュアルを横に置いて書くと良い。ちなみに本格的に VPS 側で大きい JSON を回すようになってから自分は お名前.comの高性能VPS
に Pi から処理をオフロードしている。ローカルで詰まっていた処理が、メモリが潤沢な環境だと一瞬で終わる。
小数の丸めに気を付ける
jq は内部で double を使う。0.1 + 0.2 が 0.30000000000000004 になるアレが普通に起きる。金額計算には使わない、または * 100 | round でセント/銭に正規化してから扱う。自動売買の板情報をさばいていたとき、これで 1 円ずれる取引シグナルを出して青ざめたことがある。
null の扱いがキモい
jq の null は「存在しない」と「明示的な null」の両方を指す。// で吸収するか、select(. != null) で弾くか、最初に方針を決めておくこと。決めずに書き続けると、同じスクリプト内で挙動が不整合になる。
~/.jqrc で自分の関数を育てる
毎日使うフィルタは ~/.jq に関数化しておける。
# ~/.jq
def secs_to_jst: . + 9*3600 | todate;
def only_filled: map(select(.status == "filled"));
def price_stats: {min: min, max: max, avg: (add / length)};
これで jq '.orders | only_filled | map(.price) | price_stats' のように短く書ける。コマンドはツールの使い方をあなたのワークフローに寄せていく過程そのもので、jq はその余地が大きい。他の CLI ツールを極める話は DevTools に小物を並べているので、気になれば覗いてほしい。
今日から使える 3 つの習慣
長々書いたが、まず定着させるのはこの 3 つでいい。
- JSON を目視する前に
| jq '.'を付けるクセを指に覚えさせる - シェル変数は必ず
--arg/--argjson経由で渡す - 条件分岐が必要になったら迷わず
-e
残りは必要になったときに調べれば足りる。jq は「全部覚えないと使えない」と誤解されがちなツールだが、普段使う機能は意外と少ない。grep で JSON を漁る習慣を今日でやめる、それだけで元は取れる。