🖨️

jqのstreamオプションを使ってJSONの構造を確認する

に公開

jqは、JSONを処理するコマンドラインツールです。

jqのstreamingオプションを利用して、自分で書くのは少し面倒なことを楽に行う方法を紹介します。

  • 与えられた値を示すパス配列をすべて列挙する
  • 2つのJSONの構造を比較する

まずはjqコマンドの基本的な使い方を紹介します。その後、streamオプションの紹介をし、上の2つを行う方法を示します。

jqの基本的な使い方

公式のTutorialに沿って、以下のJSONファイルを使用して説明します。

サンプルのJSONのダウンロードと確認
$ curl 'https://api.github.com/repos/jqlang/jq/commits?per_page=5' -o response.json -s
$ cat response.json | head
[
  {
    "sha": "859a8073ee8a21f2133154eea7c2bd5e0d60837f",
    "node_id": "C_kwDOAE3WVdoAKDg1OWE4MDczZWU4YTIxZjIxMzMxNTRlZWE3YzJiZDVlMGQ2MDgzN2Y",
    "commit": {
      "author": {
        "name": "dependabot[bot]",
        "email": "49699333+dependabot[bot]@users.noreply.github.com",
        "date": "2025-06-02T21:24:14Z"
      },

https://api.github.com/repos/jqlang/jq/commits?per_page=5は、Commits · jqlang/jq · GitHubの最新の5commit分の情報を取得します。

例えば、0番目のcommitの情報を抽出したい場合は、以下のようにすればよいです。

0番目のcommitの抽出
$ cat response.json | jq '.[0]' | head
{
  "sha": "859a8073ee8a21f2133154eea7c2bd5e0d60837f",
  "node_id": "C_kwDOAE3WVdoAKDg1OWE4MDczZWU4YTIxZjIxMzMxNTRlZWE3YzJiZDVlMGQ2MDgzN2Y",
  "commit": {
    "author": {
      "name": "dependabot[bot]",
      "email": "49699333+dependabot[bot]@users.noreply.github.com",
      "date": "2025-06-02T21:24:14Z"
    },
    "committer": {

もう少し複雑な例を示します。以下では、最新の5commitのmessageを取得します。

commit messageの取得
$ cat response.json | jq '.[].commit.message'
"build(deps): bump jsonschema from 4.23.0 to 4.24.0 in /docs (#3332)"
"Fix build on old Macs (#3336)"
"Update jq documentation for jq 1.8.0 release (#3333)"
"Update signatures of 1.8.0 (#3331)\n\nCo-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>"
"Update NEWS.md and AUTHORS for 1.8.0 (#3330)"

また、JSONを生成することもできます。

JSONの生成
$ cat response.json | jq -c '.[] | {name: .commit.author.name , message: .commit.message}'
{"name":"dependabot[bot]","message":"build(deps): bump jsonschema from 4.23.0 to 4.24.0 in /docs (#3332)"}
{"name":"Binbin Qian","message":"Fix build on old Macs (#3336)"}
{"name":"itchyny","message":"Update jq documentation for jq 1.8.0 release (#3333)"}
{"name":"github-actions[bot]","message":"Update signatures of 1.8.0 (#3331)\n\nCo-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>"}
{"name":"itchyny","message":"Update NEWS.md and AUTHORS for 1.8.0 (#3330)"}

stream出力

jqのstreamオプションを利用すると、JSONを[パス配列, それが示す値]の組に変換することができます。[1]

stream出力とその確認
$ cat response.json | jq -c --stream | head
[[0,"sha"],"859a8073ee8a21f2133154eea7c2bd5e0d60837f"]
[[0,"node_id"],"C_kwDOAE3WVdoAKDg1OWE4MDczZWU4YTIxZjIxMzMxNTRlZWE3YzJiZDVlMGQ2MDgzN2Y"]
[[0,"commit","author","name"],"dependabot[bot]"]
[[0,"commit","author","email"],"49699333+dependabot[bot]@users.noreply.github.com"]
[[0,"commit","author","date"],"2025-06-02T21:24:14Z"]
[[0,"commit","author","date"]]
[[0,"commit","committer","name"],"GitHub"]
[[0,"commit","committer","email"],"noreply@github.com"]
[[0,"commit","committer","date"],"2025-06-02T21:24:14Z"]
[[0,"commit","committer","date"]]

ただし、上記のコードブロックの最後の行のように、配列やオブジェクトの最後には、パスのみが格納されます。

本来は、巨大なJSONに対して逐次処理を行うためのオプションですが、このパス配列を列挙する部分を利用します。

また、jq --streamjq 'tostream'と等価です。以下、この記法も断りなく使用することにします。[2]

与えられた値を示すパス配列をすべて列挙する

streamオプションで[パス配列, それが示す値]を得られることは分かったので、適当に加工すればそれが示す値が与えられた値であるものだけを抽出できます。

以下は、dependabot[bot]が値であるようなパス配列を列挙しています。[3]

与えられた値を示すパス配列をすべて列挙する
$ cat response.json | jq -c 'tostream | {path: .[0], leaf_value: .[1]} | select(.leaf_value=="dependabot[bot]") | .path'
[0,"commit","author","name"]
[0,"author","login"]

見出しとしては与えられた値を示すと書いていますが、もう少し複雑な条件で検索することも可能であると分かります。

2つのJSONの構造を比較する

まず、パス配列だけを取り出すには以下のようにすればよいです。sort | uniq をしているのは配列やオブジェクトの最後に値を持たないパス配列が出力され、パス配列が重複するためです。[4]

パス配列だけを取り出す
$ cat response.json | jq -c 'tostream | .[0]' | sort | uniq | head
[0,"author","avatar_url"]
[0,"author","events_url"]
[0,"author","followers_url"]
[0,"author","following_url"]
[0,"author","gists_url"]
[0,"author","gravatar_id"]
[0,"author","html_url"]
[0,"author","id"]
[0,"author","login"]
[0,"author","node_id"]

これを2つのJSONに対して行えばよいです。以下では、0番目のcommitと1番目のcommitのJSONの構造を比較し、差分を確認しています。

2つのJSONの構造を比較する
$ cat response.json | jq -c '.[0] | tostream | .[0]' | sort | uniq > path0.txt
$ cat path0.txt | head
["author","avatar_url"]
["author","events_url"]
["author","followers_url"]
["author","following_url"]
["author","gists_url"]
["author","gravatar_id"]
["author","html_url"]
["author","id"]
["author","login"]
["author","node_id"]
$ cat response.json | jq -c '.[1] | tostream | .[0]' | sort | uniq > path1.txt
$ cat path1.txt | head
["author","avatar_url"]
["author","events_url"]
["author","followers_url"]
["author","following_url"]
["author","gists_url"]
["author","gravatar_id"]
["author","html_url"]
["author","id"]
["author","login"]
["author","node_id"]
$ diff path0.txt path1.txt

差分がある場合は以下のようになります。

2つのJSONの構造を比較する(差分ありver)
$ echo '{"a": {"b": {"c": 0}}}' | jq -c 'tostream | .[0]' | sort | uniq > path0.txt
$ echo '{"a": {"b": {"c": {"d": 0}}}}' | jq -c 'tostream | .[0]' | sort | uniq > path1.txt
$ diff path0.txt path1.txt
0a1
> ["a","b","c","d"]

あとがき

jq 1.8 Manualを漫然と読んでいて、Streamingの項を見たときに、こういうことができそうだなと思いつき、そのまま記事にしました。

あまり使い道はない気がしますが、やっていることとしてはけっこう面白いなと思っています。

脚注
  1. パス配列というのは微妙な表現である気もしますが、本家のJSON keypathではない何かなので、こう呼ぶことにしています ↩︎

  2. 実用的には、jqのパイプ機能を利用できるので、tostreamのほうが便利です ↩︎

  3. awkを使うでもgrepを使うでも他でも構わないのですが、せっかくなのでjqだけを使うようにしています ↩︎

  4. なくても動くのですが、重複があるのは嫌な気がします ↩︎

Discussion