はじめに
APIのレスポンスやログを jq で絞り込むことはよくあります。しかし JSON の階層が深くなると、クエリを書く前に「どこが配列で、どこがオブジェクトか」をまず把握しなければならず、途端にややこしくなります。
expected an object but got: array (...)
.orders.items と書いてこう言われ、JSON を見に戻り、[] を足してもう一度…。そうやってターミナルとエディタを往復した経験のある方も多いと思います。
そこで、JSONの構造(スキーマ)を横に表示したまま、jqクエリを書き換えるたびに結果がすぐ出るアプリを、Claude Code に作ってもらいました。
この記事では、
- 深い階層のJSONを扱うための jq の基本
- 作ったアプリ「shiboq」で工夫した点
- Claude Code と一緒に作ってみて感じたこと
を紹介します。
作ったもの: shiboq
shiboq(しぼきゅー)は、「絞る」と jq の「q」を合わせた名前です。
| 版 | 入手先 |
|---|---|
| Web版(インストール不要) | https://shiboq.e17.click/ |
| Windows版 | Microsoft Store |
| ソースコード | https://github.com/morststs/shiboq (MIT License) |
画面は5つのペインに分かれています。
| 左 | 中央 | 右 |
|---|---|---|
|
保存 / サンプル 選ぶとJSONとクエリを復元 |
JSON(上) 貼り付け・編集・整形 スキーマ(下) キーと型のツリー 使用中のキーをハイライト |
jqクエリ(上) キー名と関数を補完 実行結果(下) エラー中は前回の結果を表示 |
JSON を貼り付けてクエリを書くと、入力が止まって 500ms 後に自動で実行されます。実行ボタンはありません。
Web版はjqエンジンを WebAssembly にしてブラウザ内で動かしているので、貼り付けたJSONはどこにも送信されません。業務のJSONでも試しやすいようにしました。
jqの基本(深い階層のJSONで)
以下、次のような「お店の商品と注文」のJSONを例にします(shiboq の学習用サンプルに入っているものです)。
{
"store": "しぼQ商店",
"products": [
{ "id": 1, "name": "りんご", "category": "果物", "price": 120, "stock": 30, "tags": ["国産", "旬"] },
{ "id": 2, "name": "バナナ", "category": "果物", "price": 98, "stock": 0, "tags": ["輸入"] }
// …全5件
],
"orders": [
{
"id": "A-001",
"customer": { "name": "佐藤", "email": "sato@example.com" },
"items": [ { "productId": 1, "qty": 3 }, { "productId": 4, "qty": 1 } ],
"status": "shipped"
},
{
"id": "A-002",
"customer": { "name": "鈴木", "email": null },
"items": [ { "productId": 5, "qty": 2 } ],
"status": "pending"
}
// …全3件
]
}
shiboq のスキーマペインでは、これが次のように表示されます。
store: string
products: array<object>
id: number
name: string
category: string
price: number
stock: number
tags: array<string>
orders: array<object>
id: string
customer: object
name: string
email: string
items: array<object>
productId: number
qty: number
status: string
jq で迷うのは、たいてい**「array と object の境目」**です。それを頭に置いて見ていきます。
1. 値を取り出す . と .key
.store
# => "しぼQ商店"
.orders[0].customer.name
# => "佐藤"
. は「いま手元にある値」です。オブジェクトは .キー名、配列は [添字] でたどります。
2. 配列を展開する .[]
.products[].name
# => "りんご"
# "バナナ"
# "にんじん"
# "牛乳"
# "チーズ"
[] は配列の要素を 1つずつ別々の結果として 出します。出力が5つになっていることに注意してください。
ここが、冒頭のエラーの正体です。
.orders.items
# => エラー: expected an object but got: array (...)
スキーマを見ると orders: array<object> です。配列に .items は無いので、array の後ろには [] が必要です。
.orders[].items[].productId
# => 1 4 5 2 3 1
深いJSONでは「スキーマで array と書いてある階層には [] を付ける」と覚えておくと、迷う回数がかなり減ります。
3. パイプ | でつなぐ
.orders[] | .customer | .name
| は「左の結果を1つずつ右に渡す」です。.orders[].customer.name と同じ意味ですが、長くなったらパイプで区切ったほうが読みやすくなります。
4. 結果を配列にまとめる [ ]、オブジェクトを組み立てる { }
[.products[].name]
# => ["りんご","バナナ","にんじん","牛乳","チーズ"]
.products[] | {name, price}
# => {"name":"りんご","price":120}
# {"name":"バナナ","price":98} …
.[] でバラバラになったものを [ ] で囲むと、配列に戻せます。{name, price} は {name: .name, price: .price} の省略形です。
深い階層から必要な値だけを平らにして取り出すときは、これがいちばん使えます。
.orders[] | {id, name: .customer.name, "点数": (.items | map(.qty) | add)}
# => {"id":"A-001","name":"佐藤","点数":4}
# {"id":"A-002","name":"鈴木","点数":2}
# {"id":"A-003","name":"高橋","点数":9}
キー名に日本語を使うときは "点数" のように引用符が必要です。{点数: ...} や .会員 は構文エラーになります。取り出すときは ."会員" または .["会員"] と書きます。
5. 絞り込む select
.products[] | select(.stock == 0) | .name
# => "バナナ"
.orders[] | select(.customer.email == null) | .id
# => "A-002"
select(条件) は、条件が真のものだけを通します。
6. 集計する map / add / group_by
[.orders[].items[].qty] | add
# => 15
.products | group_by(.category) | map({category: .[0].category, "件数": length})
# => [{"category":"乳製品","件数":2},{"category":"果物","件数":2},{"category":"野菜","件数":1}]
「2階層下の値を全部集めて合計する」は、[ ... ] で集めてから add すると書けます。
7. 別の配列と突き合わせる as $変数 と INDEX
注文の items には productId しか無く、値段は products 側にあります。こういうときは、先に products を id で引ける形にして変数に入れておきます。
INDEX(.products[]; .id) as $p
| .orders[]
| {id, "合計": (.items | map($p[.productId | tostring].price * .qty) | add)}
# => {"id":"A-001","合計":570}
# {"id":"A-002","合計":900}
# {"id":"A-003","合計":868}
.orders[] に入ったあとでは . が注文1件になってしまい、.products を参照できません。階層を下りる前に as $p で取っておくのがコツです。
8. どこにあるか分からないキーを探す ..
[.. | .email? // empty]
# => ["sato@example.com","takahashi@example.org"]
.. はすべての階層を再帰的にたどります。「email がどこかにあるはずだけど階層が分からない」というときの最初の一手に便利です。
深い階層でハマりやすいところ
| 症状 | 原因 | 対処 |
|---|---|---|
expected an object but got: array |
配列に .key を付けた |
[] を足す |
cannot iterate over: null |
存在しないキーに [] を付けた |
キー名の綴りを確認/.foo[]?
|
エラーにならず null が出る |
キー名の打ち間違い | スキーマで実在するキーか確認する |
最後の「null が出るだけでエラーにならない」が厄介です。jq は存在しないキーを参照しても null を返すので、.custmer.name のような打ち間違いに気づきにくいのです。
これが「スキーマを横に置いて、いま参照しているキーをハイライトしたい」と思った一番の理由です。
アプリで工夫した点
1. スキーマ表示と「使用中のキー」のハイライト
スキーマペインでは、いまのクエリが参照しているキーに「使用中」バッジが付きます。
.orders[] | {id, name: .customer.name, "点数": (.items | map(.qty) | add)} のときは、次のようになります。
打ち間違えたキーはハイライトされません。.orders[] | .custmer.name(customer の綴り間違い)と書くと、結果は null が並ぶだけですが、スキーマを見ると customer が光っていないので、綴りのせいだとすぐに分かります。
判定はキー名の一致だけで行っていて、どの階層のキーかまでは区別していません。そのため上の画像でも、クエリでは使っていない products の id や name まで光っています。
参照しているキーは、正規表現で拾うのではなく、jqエンジン(gojq)でクエリをパースした構文木(AST)から取り出しています。そのため、次のような書き方の中のキーも拾えます。
"\(.name)様" # 文字列補間の中
-.price # 単項演算子
. as {customer: {name: $n}} | $n # 分解パターン
."会員" / .["会員"] # 引用符付き・ブラケット記法
一方で、.["\(.k)_id"] のように実行時にならないと決まらないキー名は対象外にし、補間の中の .k だけを拾うようにしています。
2. キー名の補完
クエリペインでは、いま読み込んでいるJSONに含まれるキー名と、よく使う組み込み関数(map・select・keys など)が補完候補に出ます。エディタは VS Code と同じ Monaco Editor です。
3. エラーになっても前回の結果を消さない
自動実行なので、.orders[ のように書いている途中のクエリは当然エラーになります。そのたびに結果が消えると、画面がちらついて落ち着きません。
そこで、エラーのときは前回成功したときの結果を残し、「前回の成功結果」バッジを付けて薄く表示するようにしました。
また、エラーの出し分けもしています。
- JSON自体が壊れている → JSON/スキーマ側にだけエラーを出す(クエリペインには出さない)
-
クエリの構文エラー →
構文エラー: … -
未定義の関数など →
コンパイルエラー: …
JSON の閉じカッコを消しただけなのにクエリ側までエラーで真っ赤になる、ということが起きないようにしています。
4. 書きかけのクエリが暴走しないように
自動実行には怖い面もあります。たとえば range(1e9) や repeat(.) は、書いている途中に一瞬でも成立すると、終わらない処理が始まってしまいます。
そのため、実行には次の上限を設けています。
- 実行時間 5秒でタイムアウト
- 出力 10,000件、または 10MB で打ち切り
Web版はさらに一工夫が必要でした。WebAssembly の Go には goroutine のプリエンプション(割り込み)が無く、def f: f; f のような CPU を握り続けるクエリでは Go側のタイムアウトが発火しません。そこで、jqエンジンを Web Worker で動かし、時間切れになったら Worker ごと terminate() するようにしました。新しいクエリが来たときに前のクエリがまだ動いていれば、それも Worker ごと止めます。
5. 「履歴」をやめて「明示的な保存」にした
最初は「実行に成功するたびに自動で履歴に残す」作りでした。ところが使ってみると、履歴が次のように埋まってしまいました。
.a
.add
.addr
.addre
.address
jq は存在しないキーでもエラーにせず null を返すので、入力途中のクエリがほぼすべて「成功」として残ってしまったのです。
そこで自動履歴はやめ、Ctrl+S(または「+ 保存」ボタン)で自分で保存したものだけが残るようにしました。名前は空でもよく、空のときはクエリがそのまま見出しになります。
また、エラー中は保存できないようにしています。表示中の結果は「前回の成功結果」なので、いまのクエリと食い違ったまま保存されてしまうからです。
6. 学習用サンプル(10章・43問)とヘルプ
左ペインの「サンプル」タブに、この記事で紹介したような例題を10章・43問入れました。選ぶとJSONとクエリが読み込まれ、解説と「やってみよう」を見ながらその場で書き換えられます。
- 値を取り出す
- 配列を展開する
- オブジェクトを組み立てる
- 条件で絞り込む
- 既定値・条件分岐・並べ替え
- 集計する
- 変数・reduce・関数
- 文字列と正規表現
- 出力の形式(
@csvなど) - 更新と探索(
|=・del・..・paths)
サンプルはすべて、テストで実際のjqエンジンに通して、エラーにならないこと・結果が空でないことを確認しています。サンプルを作っている途中で、日本語のキーを引用符なしで書いた例が軒並み構文エラーになり、このテストで気づきました(前述の「日本語キーには引用符が必要」は、こうして身をもって学んだことです)。
ヘルプには、構文の早見表と「困ったとき」のQ&Aのほか、使えない機能も書いています。shiboq は jq 本体ではなく gojq を内蔵しているため、jq と少し違うところがあるからです。
- jq 1.7 の仕様に準拠(
trim・tobooleanなど jq 1.8 で追加された関数の一部も使える) -
input/inputs・debug・$__loc__などは使えない - 正規表現のフラグは
g/i/mのみ(Go のregexpを使っているため) - エラーメッセージの文面は jq と異なる
7. その他の細かいところ
-
生出力(
-r相当)の切り替え:@csvや@tsvの結果をそのまま読めます。 -
JSONの整形ボタン:
JSON.parse→JSON.stringifyではなく、エディタのフォーマッタで整形します。前者だと大きな整数の精度が落ちたり1.0が1になったりするうえ、Ctrl+Zで戻せないためです。 -
スキーマを JSON Schema としてコピー: 推論したスキーマを
type/properties/itemsだけの JSON Schema にしてクリップボードへ。1件のサンプルからは判断できないrequiredなどは付けません。 - 外部への通信をしない: Webフォントは CDN から読み込まず、OS のフォントを使います。Microsoft Store の公式バッジも外部スクリプトを読み込むため使わず、普通のリンクにしました。
- jqのインストール不要: gojq を組み込んでいるので、jq コマンドが入っていない環境でも動きます。
技術構成
| デスクトップアプリ | Wails v2(Go) |
| jqエンジン | itchyny/gojq(純Go実装) |
| フロントエンド | Svelte 5(runes)+ Vite |
| エディタ | Monaco Editor |
| UI | Flowbite Svelte + Tailwind CSS 4 |
| Web版 | 同じGoコードを WebAssembly にビルドし、GitHub Pages で公開 |
Web版とデスクトップ版は同じ Svelte のコードです。Vite のエイリアスで、呼び出し先を「Wailsのバインディング」か「WebAssemblyを動かすWorker」に切り替えています。jqの実行やスキーマ推論のGoコードもそのまま共用しています。
Claude Code に作ってもらって
進め方
最初に「jqを繰り返し実行して結果を確認できるアプリが欲しい」という要望と、参考にしたい自作アプリ(同じ Wails + Svelte 構成のメモアプリ)を伝え、Claude Code と相談しながら設計書と実装計画書を作りました。そのうえで実装してもらい、使ってみて気になった点を直してもらう、の繰り返しです。
開発環境は、ホストに Go や Node を入れず、すべて Docker コンテナの中でビルド・テストする構成にしました。Windows 版の exe も、コンテナ内の mingw-w64 でクロスコンパイルしています。
よかったこと
-
「なぜそうしたか」を CLAUDE.md に残してくれる: 「履歴をやめて保存にした理由」「整形で
JSON.parseを使わない理由」などが、設計判断として CLAUDE.md に書き足されていきます。次のセッションで Claude Code が同じ失敗を繰り返さず、自分で読み返しても経緯が分かります。 - テストで確かめてくれる: 前述のサンプル全件テストのように、「動くはず」ではなく実際に動かして確かめる仕組みを作ってくれました。Web版も、ヘッドレスブラウザで画面を操作して動作確認しています。
- 実装以外も手伝ってくれる: GitHub Actions でのビルド、GitHub Pages への公開、Microsoft Store 提出用の MSIX パッケージ作成、プライバシーポリシーやストアの説明文の下書きまで手伝ってもらいました。
人間が決めたこと
一方で、次のような判断は自分で行いました。
- 自動履歴をやめて明示的な保存にする
- 外部スクリプトを読み込む公式バッジは使わない
- Store への申請は全自動ではなく、半自動にとどめる
また、Windows の実機での確認(スマート アプリ コントロールに未署名の exe がブロックされた件など)も自分で行いました。この件がきっかけで、Microsoft が署名してくれる Store 経由での配布を選んでいます。
おわりに
深い階層のJSONを jq で扱うときのコツは、
- スキーマで
arrayの階層を確認して[]を付ける - 階層を下りる前に、あとで使う値を
as $xで取っておく nullが出たら、まずキー名の綴りを疑う
の3つだと思っています。shiboq は、この3つを「目で見て確かめながら」試せるように作りました。
Web版はインストール不要ですぐ試せます。jq の練習にも、手元のJSONの調査にも使ってみてください。



