
jq の使い方入門 - JSON をコマンドラインで整形・抽出・変換する基本とハマりどころ
当サイトは Amazon.co.jp を宣伝しリンクすることで紹介料を得る手段を提供する、Amazonアソシエイト・プログラムの参加者です。価格・在庫はリンク先の最新情報をご確認ください。
APIのレスポンス、クラウドのCLIの出力、設定ファイル、ログ。開発の現場ではJSONを目にしない日がありません。ところが、1行に詰め込まれた数千文字のJSONから「statusがpaidの注文の金額だけ合計したい」と思ったとき、grepやsedでは手に負えなくなります。
そこで使うのがjqです。jqはJSONを読み込み、短い式(フィルタ)で整形・抽出・変換して出力するコマンドです。jq .で見やすく整形するだけでも便利ですが、selectやmap、group_byまで覚えると、ちょっとした集計スクリプトをPythonで書く必要がなくなります。
この記事では、jqの考え方から、よく使うフィルタ、オプション、ファイルを書き換えるときの注意、NDJSONや巨大なファイルの扱い、終了ステータスとよくあるハマりどころまでを、実行結果付きで説明します。
NOTE
2026年11月11日に、jq 1.8 Manual、jqのダウンロードページ、GitHubのjqlang/jqのリリースノートで仕様と変更点を確認しました。コマンドの実行結果は、特に断りがなければmacOS 27.2、Homebrewのjq 1.8.1、bash 5.3.9のものです。確認時点の最新リリースはjq 1.8.2(2026年6月20日公開)で、本文の機能はすべて1.8.1で動作を確かめています。サンプルのJSONはすべて自作のダミーデータです。
先に結論: よく使う書き方の早見表
まず、使う頻度が高いものを並べます。式の意味は後の章で順に説明します。
| やりたいこと | 書き方 |
|---|---|
| 整形して表示する | jq . file.json |
| 1行に詰めて出す | jq -c . file.json |
| 文字列をクォートなしで出す | jq -r '.name' file.json |
| 入れ子のフィールドを取る | jq '.a.b.c' file.json |
| 配列の先頭を取る | jq '.[0]' file.json |
| 配列の要素を1つずつ出す | jq '.[]' file.json |
| オブジェクトのキー一覧 | jq 'keys' file.json |
| 要素数 | jq 'length' file.json |
| シェル変数を文字列で渡す | jq --arg name "$NAME" ... |
| シェル変数を数値やJSONで渡す | jq --argjson n 30 ... |
| 複数のJSONを1つの配列にまとめる | jq -s . a.json b.json |
| 入力なしでJSONを組み立てる | jq -n '...' |
パイプ|を使う式は表に入れにくいので、コードでまとめておきます。
# 条件に合う要素だけ残す
jq '.users[] | select(.active)' users.json
# 配列の各要素を変換する
jq '.users | map(.name)' users.json
# 必要なフィールドだけのオブジェクトにする
jq '.users[] | {name, role}' users.json
# CSVにする(-r が必要)
jq -r '.users[] | [.id, .name] | @csv' users.json
覚えておくと事故を防げることも3つ挙げておきます。
jq ... file.json > file.jsonはファイルを空にします。一時ファイルに書いてからmvします。--argで渡した値は必ず文字列です。数値として比べたいときは--argjsonを使います。-eを付けないと、結果がnullでもjqは終了ステータス0で終わります。
jqとは: 入力、フィルタ、出力
jqは、JSONを入力として受け取り、フィルタを適用して、結果をJSONとして出力するプログラムです。
JSONの入力(0個以上) -> フィルタ -> JSONの出力(0個以上)ここで大事なのは、入力も出力も「1個」とは限らない点です。
- 入力ファイルに
{"a":1}と{"a":2}が2つ並んでいれば、フィルタは2回実行されます。 .[]のようなフィルタは、1つの入力から複数の値を出力します。selectは条件に合わないと何も出力しません(出力0個)。
jqの式は、この「値の流れ(ストリーム)」を左から右へ加工していく書き方です。シェルのパイプでコマンドをつなぐのと似ていて、jqの中でも|で処理をつなぎます。シェルのパイプそのものの仕組みはパイプとリダイレクトの記事で説明しています。
もう1つの前提は、.(ドット)が「今の入力」を表すことです。.だけのフィルタは入力をそのまま出力するので、jq .は整形して表示するだけ、という意味になります。.nameは「今の入力のnameフィールド」です。パイプの右側では、.は左側の出力に置き換わります。この「.が指すものが場所によって変わる」感覚が、jqを読み書きするときの中心になります。
インストールとバージョン
インストール
公式のダウンロードページには、主な環境向けに次のコマンドが載っています。
# macOS(Homebrew)
brew install jq
# Debian / Ubuntu
sudo apt-get install jq
# Windows(winget)
winget install jqlang.jq
# Docker(インストールせずに試す)
docker run -i --rm ghcr.io/jqlang/jq -n 'range(3)'GitHubのリリースページには、Linux、macOS、Windows向けの単体バイナリもあります。ブラウザで試すだけなら、公式のプレイグラウンドplay.jqlang.orgが使えます。
バージョンはjq --versionで確認します。
jq --version
/usr/bin/jq --versionjq-1.8.1
jq-1.7.1-apple手元のmacOS 27.2には、Homebrewとは別に/usr/bin/jq(1.7.1)も入っていました。PATHの順番によってはこちらが使われ、trimや@uridのような1.8で追加された関数がnot definedのエラーになります。手元のmacOS 27.2には、Homebrewとは別に/usr/bin/jq(1.7.1)も入っていました。PATHの順番によってはこちらが使われ、1.8で追加された関数が使えません。
マニュアルの関数が動かないときは、まずwhich jqとjq --versionでどのjqを使っているかを確認してください。Linuxのディストリビューションのパッケージも、最新版より古いことがあります。
バージョンごとの主な変更
jqは1.6(2018年11月)から1.7(2023年9月)まで、5年近くリリースが空きました。1.7のリリースノートでは、リポジトリがjqlangのOrganizationへ移ったことが告知されています。リリースノートから、この記事の内容に関係する変更を抜き出します。
| バージョン | 公開日 | 主な変更 |
|---|---|---|
| 1.7 | 2023-09-06 | リポジトリがjqlangへ移動。pick、abs、--raw-output0を追加。elseのないifを許可。数値リテラルの精度を保つように。NO_COLORに対応。--argfileを削除 |
| 1.7.1 | 2023-12-13 | 1.7の修正版 |
| 1.8.0 | 2025-06-01 | 番号を1.X.Y形式に。trim、ltrim、rtrim、trimstr、toboolean、@urid、add/1、skip/2を追加。エラーメッセージにフィルタ内の位置を表示。ホームページをjqlang.orgへ移動 |
| 1.8.1 | 2025-07-01 | セキュリティ修正。1.8.0で入れたreduceとforeachの変更を性能低下のため取り消し |
| 1.8.2 | 2026-06-20 | 多数のCVEの修正。Windows arm64版を追加。@uriと@uridのマルチバイト文字の不具合を修正 |
1.8.0には互換性に関わる変更もあります。indexやindicesが文字列のバイト位置ではなく文字(コードポイント)の位置を返すようになり、tonumberは前後に空白がある文字列をエラーにするようになりました。古いスクリプトを1.8で動かすときは、1.8.0のリリースノートの該当箇所に目を通しておくと安心です。1.8.2はセキュリティ修正が多いので、信頼できない入力を扱う環境では更新しておくのがよいでしょう。
サンプルデータ
以降の例では、次の2つのファイルを使います。users.jsonはチームのメンバー一覧、orders.ndjsonは1行に1つのJSONが並ぶ注文ログです。
{
"team": "sample-dev",
"updated": "2026-11-01",
"users": [
{"id": 1, "name": "Alice", "role": "admin", "age": 34, "tags": ["go", "infra"], "email": "alice@example.com", "active": true},
{"id": 2, "name": "Bob", "role": "member", "age": 28, "tags": ["js"], "email": null, "active": false},
{"id": 3, "name": "Carol", "role": "member", "age": 41, "tags": ["python", "js"], "email": "carol@example.com", "active": true},
{"id": 4, "name": "Dave", "role": "guest", "age": 23, "tags": [], "active": true}
]
}{"order_id":"A-001","user_id":1,"amount":1200,"status":"paid"}
{"order_id":"A-002","user_id":3,"amount":800,"status":"paid"}
{"order_id":"A-003","user_id":1,"amount":300,"status":"refunded"}
{"order_id":"A-004","user_id":2,"amount":4500,"status":"paid"}Bobはemailがnull、Daveはemailのキー自体がなく、tagsも空です。この違いが後で効いてきます。
整形と出力の形
.で整形、-cで1行に
.は入力をそのまま返すフィルタです。jqは出力するときに整形するので、結果として読みやすいJSONになります。
jq . users.json | head -5
jq -c . users.json | cut -c1-80{
"team": "sample-dev",
"updated": "2026-11-01",
"users": [
{
{"team":"sample-dev","updated":"2026-11-01","users":[{"id":1,"name":"Alice","rol-c(--compact-output)は1つのJSONを1行で出します。ログに書き出したり、次のコマンドへ1行ずつ渡したりするときに使います。-S(--sort-keys)を付けると、オブジェクトのキーをアルファベット順に並べ替えます。差分を取る前に順番をそろえたいときに便利です。
-rで文字列のクォートを外す
文字列を出力すると、jqはJSONとして正しい形、つまりダブルクォート付きで出します。シェルの変数に入れたり、別のコマンドに渡したりするときは、-r(--raw-output)でクォートを外します。
jq '.team' users.json
jq -r '.team' users.json"sample-dev"
sample-dev-rが効くのは文字列だけです。数値やオブジェクトはそのままJSONで出ます。
フィールドと配列の取り出し
.a.bでフィールドをたどる
.nameでフィールドを取り出し、.a.bのようにつなげると入れ子をたどれます。
jq '.users[0].name' users.json
jq '.nothing' users.json
jq '.nothing.deeper' users.json"Alice"
null
null存在しないキーを指定してもエラーにはならず、nullが返ります。nullに対してさらに.deeperを取ってもnullです。この性質のおかげで深い場所を気軽に参照できる一方、キー名のタイプミスに気付きにくい原因にもなります。
キー名にハイフンや空白が含まれるときは、."x-y"か.["x-y"]の形で書きます。.x-yと書くと「.xからyを引く」と解釈され、エラーになります。
型が違うとエラーになる
キーがないのは問題ありませんが、文字列や数値に対してフィールドを取ろうとするとエラーです。
jq '.team.name' users.jsonjq: error (at users.json:10): Cannot index string with string "name"?を付けると、このエラーを握りつぶして何も出力しません。
jq '.team.name?' users.json
echo "exit=$?"exit=0?はtryの短縮形です。後で説明するtry ... catchと同じく、データの形がそろっていない入力を扱うときに使います。ただし、何でも?を付けると本当の異常まで見えなくなるので、付ける場所は絞ります。
.[0]、スライス、.[]
配列は.[0]のように番号で取り出します。負の数は末尾からの位置で、.[1:3]のように書くとスライス(1番目から3番目の手前まで)になります。
jq '.users[-1].name' users.json
jq '.users[1:3] | length' users.json"Dave"
2.[]は配列の要素を1つずつ別々の値として出力します。ここがjqで一番大事な記号です。
jq '.users[].name' users.json
jq -r '.users[].name' users.json"Alice"
"Bob"
"Carol"
"Dave"
Alice
Bob
Carol
Dave.users[].nameは「users配列の各要素について.nameを取る」という意味で、結果は配列ではなく4つの文字列です。オブジェクトに.[]を使うと、値が1つずつ出てきます。
配列やオブジェクトでないものに.[]を使うとエラーになります。.[]?にするとエラーの代わりに何も出しません。
jq '.team[]' users.jsonjq: error (at users.json:10): Cannot iterate over string ("sample-dev")パイプ|とカンマ,
パイプで処理をつなぐ
A | Bは「Aの出力それぞれを入力にしてBを実行する」です。Aが4つの値を出せば、Bは4回実行されます。
jq '.users[] | .name' users.jsonこれは.users[].nameと同じ結果になります。短い式では.a.b、処理を段階に分けたいときは|、と使い分けます。
カンマで複数の値を出す
A, Bは「Aの出力の後にBの出力を続ける」です。1つの入力から複数のものを取り出したいときに使います。
jq '.users[] | .name, .age' users.json | head -4"Alice"
34
"Bob"
28|はカンマより優先順位が低い点に注意します。.users[] | .name, .ageは.users[] | (.name, .age)と解釈されます。迷ったら括弧を付けるのが確実です。
配列とオブジェクトを組み立てる
[...]で配列にまとめる
[ 式 ]は、式が出力した値をすべて集めて1つの配列にします。.[]でばらした値を、もう一度配列に戻すときの書き方です。
jq -c '[.users[].name]' users.json["Alice","Bob","Carol","Dave"]{...}でオブジェクトを作る
オブジェクトは{キー: 式}の形で作ります。キーと同じ名前のフィールドを取り出すだけなら、{name, role}のように省略できます。
jq -c '.users[] | {name, role}' users.json
jq -c '.users[] | {user: .name, tag_count: (.tags | length)}' users.json{"name":"Alice","role":"admin"}
{"name":"Bob","role":"member"}
{"name":"Carol","role":"member"}
{"name":"Dave","role":"guest"}
{"user":"Alice","tag_count":2}
{"user":"Bob","tag_count":1}
{"user":"Carol","tag_count":2}
{"user":"Dave","tag_count":0}値の部分にパイプを書くときは、(.tags | length)のように括弧で囲みます。キーを式で決めたいときは、キー側を括弧で囲みます。
jq -c '{(.team): (.users | length)}' users.json{"sample-dev":4}selectで絞り込む
条件に合う値だけを通す
select(条件)は、条件が真なら入力をそのまま出し、偽なら何も出しません。.[]と組み合わせて、配列から要素を絞り込むのが定番です。
jq -r '.users[] | select(.age >= 30) | .name' users.json
jq -r '.users[] | select(.role == "member" and .active) | .name' users.jsonAlice
Carol
Carol比較は==、!=、<、<=、>、>=、論理演算はand、or、notです。notは関数なので、.active | notのようにパイプで渡して使います。
# 正規表現にマッチするもの
jq -r '.users[] | select(.name | test("^[AC]")) | .name' users.json
# 候補のどれかに一致するもの
jq -c '[.users[] | select(.role | IN("admin", "guest")) | .name]' users.jsonAlice
Carol
["Alice","Dave"]testの正規表現はOnigurumaというライブラリで処理されます。testの正規表現はOniguruma(PCREに近い書き方)です。正規表現の書き方そのものは正規表現の実践ガイドを参照してください。
jqで偽になるのはfalseとnullだけ
jqでは、偽として扱われるのはfalseとnullの2つだけです。0や空文字列、空配列は真になります。JavaScriptやPythonの感覚で書くと間違えるところです。
jq -n -c '0, "", [], {}, null, false | if . then "truthy" else "falsy" end'"truthy"
"truthy"
"truthy"
"truthy"
"falsy"
"falsy"たとえばselect(.tags | index("js"))は、"js"が配列の0番目にあるとindexが0を返します。0は真なので正しく残りますが、「見つからなければnull」という仕様に頼った書き方です。意図が読み取りやすいanyやINを使うほうが安全です。
絞り込んだ件数を数える
selectの結果は配列ではないので、件数を数えるときは[...]で囲んでからlengthを使います。
jq '[.users[] | select(.active)] | length' users.json3mapと集計
mapで配列を変換する
map(f)は、配列の各要素にfを適用した新しい配列を返します。中身は[.[] | f]と同じです。
jq -c '.users | map(.age)' users.json
jq -c '.users | map(select(.active) | .name)' users.json[34,28,41,23]
["Alice","Carol","Dave"]mapの中でselectを使うと、条件に合わない要素が消えた配列になります。.[] | select(...)がばらばらの値を出すのに対して、map(select(...))は配列のまま返るので、結果をそのまま次の配列処理に渡せます。
add、length、min、max
数値の配列はaddで合計できます。平均はadd / lengthです。
jq '.users | map(.age) | add' users.json
jq '.users | map(.age) | add / length' users.json
jq -c '.users | map(.age) | min, max' users.json126
31.5
23
41addは文字列の配列なら連結、配列の配列なら結合、オブジェクトの配列なら合体します。
オブジェクトを調べる: keys、has、length、type
keysはオブジェクトのキーをアルファベット順に並べた配列を返します。元の順番が欲しいときはkeys_unsortedです。
jq -c '.users[0] | keys' users.json
jq -c '.users[0] | keys_unsorted' users.json["active","age","email","id","name","role","tags"]
["id","name","role","age","tags","email","active"]lengthは対象によって意味が変わり、配列なら要素数、オブジェクトならキーの数、文字列なら文字数です。
has("キー")はキーが存在するかを返します。値がnullでもキーがあればtrueです。
jq -c '.users[] | [.name, has("email"), .email]' users.json["Alice",true,"alice@example.com"]
["Bob",true,null]
["Carol",true,"carol@example.com"]
["Dave",false,null]Bob("email": null)とDave(キーなし)は、.emailではどちらもnullになり区別できません。区別が必要な処理ではhasを使います。値の型はtypeで調べられます。
to_entriesとwith_entries
キーと値のペアに分解する
to_entriesは、オブジェクトを{"key": キー, "value": 値}の配列に変換します。from_entriesはその逆です。キーを値として扱いたいとき、つまり「キー名で絞り込む」「キー名を書き換える」ときに使います。
jq -n -c '{"a": 1, "b": 2} | to_entries'
jq -n -c '[{"key": "x", "value": 1}, {"key": "y", "value": 2}] | from_entries'[{"key":"a","value":1},{"key":"b","value":2}]
{"x":1,"y":2}with_entriesでまとめて変換する
with_entries(f)はto_entries | map(f) | from_entriesの短縮形です。キーと値のペアを1つずつfで加工して、オブジェクトに戻します。
# 値が文字列のフィールドだけ残す
jq -c '.users[0] | with_entries(select(.value | type == "string"))' users.json
# キー名を小文字にそろえる
jq -n -c '{"Content-Type": "json", "X-Debug": "1"} | with_entries(.key |= ascii_downcase)'{"name":"Alice","role":"admin","email":"alice@example.com"}
{"content-type":"json","x-debug":"1"}並べ替えとグループ化: sort_by、group_by、unique_by
sort_by
sort_by(式)は、式の値で配列を並べ替えます。降順はreverseを続けるか、数値なら符号を反転させます。式をカンマで並べると、複数のキーで並べ替えます。
jq -c '.users | sort_by(.age) | map(.name)' users.json
jq -c '.users | sort_by(-.age) | map(.name)' users.json
jq -c '.users | sort_by(.role, .name) | map([.role, .name])' users.json["Dave","Bob","Alice","Carol"]
["Carol","Alice","Bob","Dave"]
[["admin","Alice"],["guest","Dave"],["member","Bob"],["member","Carol"]]型が混ざった値を並べると、jqはnull、false、true、数値、文字列、配列、オブジェクトの順に並べます。
jq -n -c '[{}, [], "a", 1, true, false, null] | sort'[null,false,true,1,"a",[],{}]group_byで集計する
group_by(式)は、式の値が同じ要素ごとに配列をまとめ、配列の配列を返します。グループは式の値の順に並びます。集計はたいていgroup_byの後にmapを続けます。
jq -c '.users | group_by(.role) | map({role: .[0].role, count: length})' users.json
jq -c '.users | group_by(.role) | map({key: .[0].role, value: map(.name)}) | from_entries' users.json[{"role":"admin","count":1},{"role":"guest","count":1},{"role":"member","count":2}]
{"admin":["Alice"],"guest":["Dave"],"member":["Bob","Carol"]}SQLのGROUP BY roleとCOUNT(*)に当たる処理です。.[0].roleは「グループの先頭要素のrole」で、同じグループ内ではどれも同じ値なので先頭を使います。
uniqueとunique_by
uniqueは重複を取り除いて並べ替えた配列を、unique_by(式)は式の値ごとに最初の1件を残した配列を返します。
jq -c '[.users[].tags[]] | unique' users.json
jq -c '.users | unique_by(.role) | map(.name)' users.json["go","infra","js","python"]
["Alice","Dave","Bob"]変数と外部からの値: --arg、--argjson、$ENV
シェル変数は--argで渡す
シェルの変数をjqの式に埋め込むとき、式の文字列に直接$nameを埋め込むのは避けます。--arg 名前 値で渡すと、jqの中で$名前として使えます。
jq -r --arg role member '.users[] | select(.role == $role) | .name' users.jsonBob
Carol--argの値は、クォートや改行を含んでいても安全に文字列として扱われます。
--argの値は必ず文字列
--argで渡した値は常に文字列です。数値と比べると、エラーにならないまま結果が空になります。
jq -r --arg min 30 '.users[] | select(.age >= $min) | .name' users.json
echo "---"
jq -r --argjson min 30 '.users[] | select(.age >= $min) | .name' users.json---
Alice
Carol前の章の並び順のとおり、jqでは数値は常に文字列より小さいので、34 >= "30"は偽になります。数値やJSONとして渡したいときは--argjsonを使うか、($min | tonumber)で変換します。
環境変数と$ARGS
環境変数は$ENV.名前かenv.名前で読めます。マニュアルによると、$ENVはjqが起動した時点の環境変数、envはjqの現在の環境です。
TARGET=Carol jq -r '.users[] | select(.name == $ENV.TARGET) | .id' users.json3名前付きの引数は$ARGS.namedに、--argsの後ろに並べた値は$ARGS.positionalに入ります。ファイルの中身を変数に入れる--slurpfile(JSONを配列として)と--rawfile(テキストのまま)もあります。1.6まであった--argfileは1.7で削除されたので、古い記事のコマンドを使うときは--slurpfileに置き換えます。
as $xと、.が変わる落とし穴
式の途中の値は式 as $名前 | ...で変数に入れられます。これが必要になるのは、selectなどの内側では.が別のものを指すためです。
ユーザーごとに注文の合計を出す例で確かめます。最初の書き方は間違いです。
jq -c --slurpfile orders orders.ndjson '.users[] | {name, n: ([$orders[] | select(.user_id == .id)] | length)}' users.json | head -1{"name":"Alice","n":0}select(.user_id == .id)の中の.は注文のオブジェクトなので、.idは注文のid(存在しないのでnull)です。ユーザーのidを使うには、外側で変数に入れておきます。
jq -c --slurpfile orders orders.ndjson '.users[] | .id as $id | {name, total: ([$orders[] | select(.user_id == $id) | .amount] | add // 0)}' users.json{"name":"Alice","total":1500}
{"name":"Bob","total":4500}
{"name":"Carol","total":800}
{"name":"Dave","total":0}addは空配列に対してnullを返すので、// 0で0にしています。//は後の章で説明します。
主なオプション
よく使うオプションを整理します。
| オプション | 長い名前 | 働き |
|---|---|---|
-r | --raw-output | 文字列をクォートなしで出す |
-j | --join-output | -rに加えて改行を付けない |
-c | --compact-output | 1つの結果を1行で出す |
-s | --slurp | 入力をすべて読み、1つの配列にしてから実行する |
-n | --null-input | 入力を読まず、nullを入力として1回実行する |
-e | --exit-status | 最後の出力によって終了ステータスを変える |
-R | --raw-input | 入力をJSONではなく、1行ずつの文字列として読む |
-S | --sort-keys | キーを並べ替えて出す |
| (なし) | --arg / --argjson | 変数を渡す |
| (なし) | --stream | 入力をパスと値の組に分解して読む |
-sと-n
-sは入力をすべて読んでから1つの配列にまとめます。複数のJSONをまとめて集計したいときに使います。
jq -s 'map(select(.status == "paid") | .amount) | add' orders.ndjson6500-nは入力を読まずに式を1回だけ実行します。計算や、JSONを組み立てるだけの用途で使います。
jq -n '1 + 2'
jq -n -c '{created: (now | todate | length)}'3
{"created":20}-nとinput、inputsを組み合わせると、入力を自分のペースで読めます。inputsは残りの入力をすべて順に出す関数です。-sと違って配列を作らずに1件ずつ処理できるので、件数が多いときは-nとinputsのほうがメモリを使いません。
jq -n '[inputs | .amount] | add' orders.ndjson6800-R(--raw-input)を付けると、各行をJSONではなく文字列として読みます。
フォーマット文字列と文字列補間
@csv、@tsv
@csvと@tsvは配列をCSVやTSVの1行に変換します。結果は文字列なので、-rを付けて出します。
jq -r '(["id", "name", "role"] | @csv), (.users[] | [.id, .name, .role] | @csv)' users.json
jq -r '.users[] | [.id, .name, .role] | @tsv' users.json"id","name","role"
1,"Alice","admin"
2,"Bob","member"
3,"Carol","member"
4,"Dave","guest"
1 Alice admin
2 Bob member
3 Carol member
4 Dave guest@csvは文字列をダブルクォートで囲み、中のダブルクォートを2つ重ねてエスケープします。配列の中にオブジェクトや配列があるとエラーになるので、先にスカラー値だけの配列にしておきます。
@base64、@uri、@html、@sh
ほかにも、文字列をエンコードするフォーマットがあります。
jq -r -n '"user:pass" | @base64'
jq -r -n '"dXNlcjpwYXNz" | @base64d'
jq -r -n '"東京 a&b=c" | @uri'
jq -r -n '"%E6%9D%B1%E4%BA%AC" | @urid'
jq -r -n '[1, "a b"] | @sh'dXNlcjpwYXNz
user:pass
%E6%9D%B1%E4%BA%AC%20a%26b%3Dc
東京
1 'a b'@base64dはBase64のデコード、@urid(1.8.0以降)はパーセントエンコーディングのデコードです。@shは値をシェルで安全に使える形にクォートします。Base64とパーセントエンコーディングの仕組みは、Base64の記事とURLエンコードの記事で説明しています。JWTのペイロードを覗くときにも@base64dが使えますが、JWTはBase64URLなので記号の置き換えが必要です。詳しくはJWTの記事を参照してください。
\(...)で文字列に値を埋め込む
文字列の中に\(式)を書くと、式の結果が埋め込まれます。
jq -r '.users[] | "\(.name) (\(.age)) <\(.email // "no-email")>"' users.jsonAlice (34) <alice@example.com>
Bob (28) <no-email>
Carol (41) <carol@example.com>
Dave (23) <no-email>フォーマットと補間を組み合わせて@uri "...\(...)"と書くと、埋め込む部分だけがエンコードされます。URLのクエリを作るときは、値をエンコードしてから埋め込みます。
jq -r -n --arg q 'jq 使い方' '"https://example.com/search?q=\($q | @uri)"'https://example.com/search?q=jq%20%E4%BD%BF%E3%81%84%E6%96%B9代替演算子//
a // bは、aがfalseかnull(か何も出さない)ときにbを返します。既定値の設定に使います。
jq -r '.users[] | .email // "(none)"' users.jsonalice@example.com
(none)
carol@example.com
(none)注意点は、falseも置き換えの対象になることです。0や空文字列は真なので置き換わりません。
jq -n -c 'false // "default", (0 // "default"), ([] | .[0] // "empty")'"default"
0
"empty".active // trueのように書くと、activeがfalseのユーザーまでtrueになります。真偽値のフィールドに既定値を入れるときは、if has("active") then .active else true endのように書き分けます。.email // emptyと書けば、値がないものを出力から取り除けます。
値を書き換える: del、=、|=
delでフィールドを消す
del(パス)は指定したパスを取り除きます。カンマで複数指定でき、selectと組み合わせて配列の要素も消せます。
jq -c '.users[0] | del(.email, .tags)' users.json
jq -c 'del(.users[] | select(.active | not)) | .users | map(.name)' users.json{"id":1,"name":"Alice","role":"admin","age":34,"active":true}
["Alice","Carol","Dave"]=と|=の違い
jqの代入は、入力全体を受け取って「一部を書き換えた全体」を返します。元のファイルが書き換わるわけではありません。
パス = 値: 右辺は元の入力全体を.として評価されるパス |= 式: 右辺はそのパスの今の値を.として評価される
jq -c '.users[].name |= ascii_upcase | [.users[].name]' users.json
jq -c '.users[0] | .age += 1 | {name, age}' users.json["ALICE","BOB","CAROL","DAVE"]
{"name":"Alice","age":35}+=、-=、*=なども|=の仲間で、今の値に対して計算します。=と|=の違いがはっきり出る例が次です。
jq -c '.users[0] | .name = .role | {name, role}' users.json
jq -c '.users[0] | .name |= .role' users.json{"name":"admin","role":"admin"}
jq: error (at users.json:10): Cannot index string with string "role"=では右辺の.roleがユーザーのオブジェクトから取られます。|=では右辺の.が.nameの値(文字列"Alice")になるので、文字列から.roleを取ろうとしてエラーになります。
パスの左辺にselectを書くと、条件に合う場所だけを書き換えられます。存在しないパスに代入すると、途中のオブジェクトも作られます。
jq -c '(.users[] | select(.role == "guest") | .role) = "member" | [.users[].role]' users.json
jq -c '.meta.version = "2" | .meta' users.json["admin","member","member","member"]
{"version":"2"}reduceで畳み込む
reduce 値の列 as $x (初期値; 更新式)は、値を1つずつ取り出して、状態を更新していきます。更新式の中の.は今の状態です。
jq -n 'reduce inputs as $o (0; . + $o.amount)' orders.ndjson
jq -c -n 'reduce inputs as $o ({}; .[$o.status] += 1)' orders.ndjson
jq -c '.users | reduce .[] as $u ({}; .[$u.role] += [$u.name])' users.json6800
{"paid":3,"refunded":1}
{"admin":["Alice"],"member":["Bob","Carol"],"guest":["Dave"]}2つ目の例は、ステータスごとの件数を数えています。.[$o.status]がまだない(null)ときも、null + 1は1になるので初期化が要りません。group_byは配列全体を読んで並べ替えるのに対して、reduceとinputsの組み合わせは1件ずつ処理するので、件数が多いログの集計に向いています。
ファイルを書き換えるときの注意
jqにはファイルをその場で書き換えるオプションがありません。そこで次のように書きたくなりますが、これはファイルを空にします。
wc -c work.json
jq '.team = "x"' work.json > work.json
echo "exit=$?"
wc -c work.json 529 work.json
exit=0
0 work.jsonシェルはjqを起動する前に、> work.jsonの処理としてwork.jsonを空にして開きます。jqが読みに行ったときには中身がなく、空の入力に対して何も出力しないまま正常終了します。リダイレクトの順序の詳細はパイプとリダイレクトの記事で扱っています。zshでnoclobberが有効な場合は、file existsのエラーで上書き自体が止まります。
安全な書き方は、一時ファイルに書いてから置き換える方法です。&&でつなぐと、jqが失敗したときに元のファイルが残ります。
tmp=$(mktemp) && jq '.team = "sample-ops"' work.json > "$tmp" && mv "$tmp" work.json
jq -r .team work.jsonsample-opsmoreutilsのspongeを使う方法もあります。spongeは標準入力をすべて読み終えてからファイルに書き込むので、jq '...' work.json | sponge work.jsonと書けます。ただし、パイプの前段のjqが失敗して何も出力しなかった場合にspongeがどう振る舞うかは、この記事では確かめていません。失敗時に中身が消えないことを手元で確認してから使ってください。ただし、パイプの途中でjqが失敗してもspongeは空の入力をそのまま書き込むので、失敗時に中身が消えないかは確認してから使ってください(set -o pipefailだけでは防げません)。
curlと組み合わせる
APIのレスポンスから必要なところだけ取る
jqが一番活躍するのは、HTTP APIのレスポンスを読むときです。GitHubのREST APIで、jq自身のリポジトリの情報を取ってみます。
curl -sS https://api.github.com/repos/jqlang/jq | jq '{name: .full_name, license: .license.spdx_id, default_branch, html_url}'{
"name": "jqlang/jq",
"license": "NOASSERTION",
"default_branch": "master",
"html_url": "https://github.com/jqlang/jq"
}リリースの一覧から、正式版のタグと公開日をTSVで出すこともできます。
curl -sS 'https://api.github.com/repos/jqlang/jq/releases?per_page=5' | jq -r '.[] | select(.prerelease | not) | [.tag_name, (.published_at | .[0:10])] | @tsv'jq-1.8.2 2026-06-20
jq-1.8.1 2025-07-01
jq-1.8.0 2025-06-01
jq-1.7.1 2023-12-13
jq-1.7 2023-09-06この結果は2026年11月11日時点のもので、新しいリリースが出れば変わります。認証なしのリクエストは回数に制限があり、確認時のレスポンスヘッダーのx-ratelimit-limitは60でした。APIの設計の考え方はREST APIの設計の記事、エラー時のステータスコードはHTTPステータスコードの記事で扱っています。
失敗を見逃さない
curlとjqをパイプでつなぐと、curlの失敗が見えにくくなります。存在しないリポジトリを指定すると、GitHubは{"message": "Not Found", ...}のようなエラーのJSONを返すので、jqはそれを普通に処理してしまいます。curlに-f(--fail)を付けるとHTTPのエラーでcurlが失敗し、jqには何も渡りません。jqは空の入力では何も出さずに終了ステータス0で終わるので、パイプライン全体としては成功に見えます。スクリプトではset -o pipefailを使い、さらに値が取れなかったことをjq -eで検出します。pipefailについてはset -euo pipefailの記事で詳しく説明しています。
set -euo pipefail
tag=$(curl -fsS https://api.github.com/repos/jqlang/jq/releases/latest | jq -er '.tag_name')
echo "latest: $tag"latest: jq-1.8.2NDJSON(JSON Lines)を扱う
1行に1つのJSONを並べた形式は、JSON LinesやNDJSONと呼ばれ、ログやデータの書き出しでよく使われます。jqは入力に複数のJSONが並んでいれば順に処理するので、NDJSONはそのまま読めます。
jq -c 'select(.amount > 1000)' orders.ndjson{"order_id":"A-001","user_id":1,"amount":1200,"status":"paid"}
{"order_id":"A-004","user_id":2,"amount":4500,"status":"paid"}全体を集計するときは、-sで配列にするか、-nとinputsまたはreduceを使います。
jq -s -c 'group_by(.user_id) | map({user_id: .[0].user_id, total: (map(.amount) | add)})' orders.ndjson[{"user_id":1,"total":1500},{"user_id":2,"total":4500},{"user_id":3,"total":800}]逆に、配列をNDJSONにしたいときは.[]と-cを組み合わせます(jq -c '.users[]' users.json)。
途中に壊れた行が混ざっていると、jqはそこでパースエラー(終了ステータス5)になって止まります。壊れた行を飛ばしたいときは、-Rで行を文字列として読み、fromjson?で1行ずつ解釈します。
printf '{"a":1}\n{bad}\n{"a":3}\n' | jq -c -R 'fromjson? // {"error": .}'{"a":1}
{"error":"{bad}"}
{"a":3}大きなファイルと--stream
jqは通常、1つのJSONをすべてメモリに読み込んでから処理します。数GBの1つの配列のように、1つのJSONが巨大な場合は、--streamでパスと値の組に分解しながら読む方法があります。
echo '{"a":{"b":[10,20]},"c":"x"}' | jq -c --stream '.'[["a","b",0],10]
[["a","b",1],20]
[["a","b",1]]
[["a","b"]]
[["c"],"x"]
[["c"]][パス, 値]の組が葉の値、[パス]だけのものは「ここで配列やオブジェクトが閉じた」という印です。fromstreamでこれを組み立て直し、truncate_streamでパスの先頭を削ると、巨大な配列の要素を1つずつ取り出せます。
jq -n -c '{items: [range(5) | {id: ., name: "item-\(.)"}]}' > big.json
jq -c -n --stream 'fromstream(2 | truncate_stream(inputs))' big.json | head -2{"id":0,"name":"item-0"}
{"id":1,"name":"item-1"}2 | truncate_stream(...)は、パスの先頭2つ("items"と配列の番号)を削る指定です。トップレベルが配列のファイルなら1にします。--streamは書き方が難しいので、メモリに収まらない場合の手段と考えるのがよいでしょう。--streamは書き方が難しく、全体を読む方式より遅くなることもあるので、メモリに収まらない場合の手段と考えるのがよいでしょう。データを作る側を選べるなら、最初からNDJSONで書き出すほうが扱いやすくなります。
エラーと終了ステータス
終了ステータスの一覧
マニュアルとリリースノート、手元の実行結果から、jqの終了ステータスを整理します。
| 終了ステータス | 意味 |
|---|---|
| 0 | 正常に実行できた(-eの場合は、最後の出力がfalseでもnullでもない) |
| 1 | -eのとき、最後の出力がfalseかnullだった |
| 2 | 使い方の誤りやシステムエラー(ファイルが開けないなど) |
| 3 | フィルタの文法エラー(コンパイルエラー) |
| 4 | -eのとき、一度も有効な結果が出なかった |
| 5 | 実行時エラーや入力のパースエラー(halt_errorの既定値も5) |
それぞれ実際に確かめます。
jq -e '.users[] | select(.name == "Zed")' users.json; echo "exit=$?"
jq -e '.nothing' users.json; echo "exit=$?"
jq '.users[' users.json; echo "exit=$?"
jq . missing.json; echo "exit=$?"
echo '{"a":1,}' | jq .; echo "exit=$?"exit=4
null
exit=1
jq: error: syntax error, unexpected end of file at <top-level>, line 1, column 7:
.users[
^
jq: 1 compile error
exit=3
jq: error: Could not open file missing.json: No such file or directory
exit=2
jq: parse error: Expected another key-value pair at line 1, column 8
exit=51.8.0から、文法エラーのメッセージにフィルタ内の位置が^で示されるようになりました。
-eでシェルの条件に使う
-eを付けないと、結果がnullでも終了ステータスは0です。「値があるかどうか」でシェルの処理を分けたいときは、if jq -e '...' file.json > /dev/null; thenのように-eを付けます。
try ... catchとerror
try 式 catch 式で実行時エラーを捕まえられます。catchの側では、.がエラーメッセージになります。error("メッセージ")で自分でエラーを起こすこともできます。
jq -n -c 'try error("boom") catch "caught: \(.)"'
jq -c '[.users[] | try (.email | ascii_downcase) catch "N/A"]' users.json"caught: boom"
["alice@example.com","N/A","carol@example.com","N/A"]よくあるハマりどころ
フィルタはシングルクォートで囲む
jqのフィルタには$や"、|、!のようにシェルで特別な意味を持つ記号がよく出てくるので、フィルタ全体をシングルクォートで囲むのが基本です。ダブルクォートで囲むと、シェルが先に中身を解釈します。
jq -n -c "$ENV.HOME"
jq -r ".users[] | select(.name == "Alice") | .id" users.jsonnull
jq: error: Alice/0 is not defined at <top-level>, line 1, column 28:
.users[] | select(.name == Alice) | .id
^^^^^
jq: 1 compile error1つ目はシェルが$ENVを(空の)シェル変数として展開し、jqには.HOMEだけが渡っています。2つ目はシェルのクォートが途中で閉じて、"Alice"のダブルクォートが消えています。シェル変数を式の文字列に埋め込むのはやめて、--argで渡してください。値にダブルクォートが含まれると、式の文法が壊れたり、意図しない式が実行されたりします。
nullと「キーがない」と「空配列」
.emailは値がnullでもキーがなくてもnullです。また、空配列の.[0]もエラーではなくnullになります。
jq -c '[.users[] | .tags[0]]' users.json["go","js","python",null]nullに対して文字列の関数(ascii_downcaseなど)を使うとエラーになります。データの形がそろっていないときは、// emptyで除くか、//で既定値を入れるか、hasで分けるかを先に決めておきます。
数値の精度
マニュアルによると、jqの数値は内部ではIEEE 754の倍精度浮動小数点数で表されます。1.7からは、入力のリテラルを変更しなければ元の表記のまま出力するようになりましたが、計算すると倍精度に丸められます。
echo '{"id": 12345678901234567890, "price": 0.1}' > nums.json
jq -c '.id' nums.json
jq -c '.id + 0' nums.json
jq -c '.price + 0.2' nums.json
jq -n -c '9007199254740993, 9007199254740993 + 0'12345678901234567890
12345678901234567000
0.30000000000000004
9007199254740993
9007199254740992そのまま出すだけなら桁が保たれますが、+ 0をした途端に値が変わっています。2^53(9007199254740992)を超える整数は、倍精度では正確に表せません。64ビット整数のIDのように大きな値を扱うときはTwitterのIDのような大きな整数のIDを扱うときは、計算や比較をせずに出力するか、JSON側で文字列にしておくのが安全です。0.1 + 0.2が0.30000000000000004になる理由は浮動小数点数の記事で説明しています。
代替ツール
jqと同じような用途のツールもあります。確認できたものを簡潔に紹介します。
| ツール | 説明(GitHubのリポジトリの説明文から) | URL |
|---|---|---|
| gojq | Goで書かれたjqの実装 | github.com/itchyny/gojq |
| jaq | 正確さ、速さ、簡潔さを重視したjqのクローン(Rust製) | github.com/01mf02/jaq |
| yq(mikefarah版) | YAML、JSON、XML、CSV、TOMLなどを扱うGo製のプロセッサ | github.com/mikefarah/yq |
| fx | ターミナルで使うJSONビューアー兼プロセッサ | github.com/antonmedv/fx |
yqにはgojqの作者itchyny氏は、jq本体のリリースノートにも多くの変更の担当者として名前が出てくるメンテナーです。yqには、jqを内部で呼び出してYAMLを扱うPython製のkislyuk/yqという同名の別ツールもあり、文法が違うので注意が必要です。これらのツールはjqと完全に互換ではありません。細かい関数の有無や数値の扱いはそれぞれのドキュメントで確認してください。この記事ではjq以外のツールの動作は確かめていません。
まとめ
- jqは「入力 -> フィルタ -> 出力」のプログラムで、値は0個以上のストリームとして流れる。
.は今の入力を指す .a.bでフィールド、.[0]で要素、.[]で要素をばらして出す。[...]で配列に、{...}でオブジェクトに組み立て直す- 絞り込みは
select、変換はmap、キーの操作はto_entriesとwith_entries、集計はgroup_byとreduce - 偽になるのは
falseとnullだけ。//はfalseも置き換える - シェルの値は
--arg(文字列)と--argjson(JSON)で渡す。フィルタはシングルクォートで囲む jq ... f > fはファイルを空にする。一時ファイルに書いてからmvする- スクリプトでは
-eとset -o pipefailで失敗を検出する。終了ステータスは0〜5で意味が分かれる 2^53を超える整数は計算すると精度が落ちる。大きなIDは文字列で扱う
まずはjq .での整形から始め、.[]とselect、mapの3つを使えるようになると、APIのレスポンスやログの調査がかなり楽になります。式を試すときは、play.jqlang.orgやjq -nで小さく確かめながら組み立てていくのがおすすめです。


