TechQuant Blog

jq を使いこなすための実践パターン — JSON を秒で料理する日常コマンド集

7分で読める

深夜2時、Raspberry Pi 5 から叩いた証券会社の WebSocket API が 8KB くらいの JSON を吐いて、cat で開いたら端末が真っ黒になった。python3 -m json.tool でパイプして人間に読める形にして、該当フィールドを grep で探して… それを 10 回くらい繰り返したところで、そろそろ jq を真面目に覚えようと決めた。翌朝、同じ作業が 3 秒で終わった。

知っているコマンドと、実戦で使えるコマンドは違う。jq '.' で整形できることは全員が知っている。でも selectmap--arg を組み合わせて 1 行でフィルタを書けるかは、別の話だ。この記事は、自分が毎日叩いている jq のパターンを、使用頻度の高い順に並べたメモだ。

インストールは apt install jqbrew 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.20.30000000000000004 になるアレが普通に起きる。金額計算には使わない、または * 100 | round でセント/銭に正規化してから扱う。自動売買の板情報をさばいていたとき、これで 1 円ずれる取引シグナルを出して青ざめたことがある。

null の扱いがキモい

jqnull は「存在しない」と「明示的な 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 を漁る習慣を今日でやめる、それだけで元は取れる。