2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

深い階層のJSONでjqに迷子にならないよう、スキーマを見ながら試せるアプリをClaude Codeに作ってもらった

2
Posted at

はじめに

APIのレスポンスやログを jq で絞り込むことはよくあります。しかし JSON の階層が深くなると、クエリを書く前に「どこが配列で、どこがオブジェクトか」をまず把握しなければならず、途端にややこしくなります。

expected an object but got: array (...)

.orders.items と書いてこう言われ、JSON を見に戻り、[] を足してもう一度…。そうやってターミナルとエディタを往復した経験のある方も多いと思います。

そこで、JSONの構造(スキーマ)を横に表示したまま、jqクエリを書き換えるたびに結果がすぐ出るアプリを、Claude Code に作ってもらいました。

この記事では、

  1. 深い階層のJSONを扱うための jq の基本
  2. 作ったアプリ「shiboq」で工夫した点
  3. Claude Code と一緒に作ってみて感じたこと

を紹介します。

作ったもの: shiboq

shiboq(しぼきゅー)は、「絞る」と jq の「q」を合わせた名前です。

shiboq の画面。左にサンプル、中央にJSONとスキーマ、右にクエリと実行結果

版 入手先
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・customer・name・id・items・qty がハイライトされたスキーマ

打ち間違えたキーはハイライトされません。.orders[] | .custmer.name(customer の綴り間違い)と書くと、結果は null が並ぶだけですが、スキーマを見ると customer が光っていないので、綴りのせいだとすぐに分かります。

綴りを間違えた 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[ のように書いている途中のクエリは当然エラーになります。そのたびに結果が消えると、画面がちらついて落ち着きません。

そこで、エラーのときは前回成功したときの結果を残し、「前回の成功結果」バッジを付けて薄く表示するようにしました。

.orders.items で実行エラーになり、結果ペインに「前回の成功結果」バッジ付きで直前の結果が薄く残っている

また、エラーの出し分けもしています。

  • 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とクエリが読み込まれ、解説と「やってみよう」を見ながらその場で書き換えられます。

  1. 値を取り出す
  2. 配列を展開する
  3. オブジェクトを組み立てる
  4. 条件で絞り込む
  5. 既定値・条件分岐・並べ替え
  6. 集計する
  7. 変数・reduce・関数
  8. 文字列と正規表現
  9. 出力の形式(@csv など)
  10. 更新と探索(|=・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の調査にも使ってみてください。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?