シンプルなデータフォーマットとしてJSONが覇権をとって久しい昨今ですが、 人気があるがゆえにJSONを取り巻くツール類も色々とあって大変です。

今日は、雑多にJSONのツール類について書いていきます。

クエリ

JSONから特定の要素を取り出したい、という要求はやはりそれなりにあって、いろんな形式でアクセスできますね。
私なんかは多すぎて困ってしまいます。

比較のために例を交えつつ jq, JSONPath, JMESPath, JSON Pointer を紹介します。

[{
    "id": 100,
    "name": "shiro", 
    "tags": ["dog", "white"]
},{
    "id": 101,
    "name": "pochi", 
    "tags": ["dog", "brown"]
},{
    "id": 102,
    "name": "tama", 
    "tags": ["cat", "white"]
}]

こんなJSONがあったとして、

  1. 配列の先頭の要素
  2. id=101を持つ要素のnameの値
  3. tagsにwhiteが含まれる要素のidの値

が取得したいとします。

jq (yq)

https://stedolan.github.io/jq/

jq というCLIツールは、入力のJSONに独自のクエリを書くことで抽出などの処理を書くことができます。
jq 1.0 は 2015年8月公開のようです。(現在はjq 1.6)

後発?の yq というCLIもあって、こちらはJSONの他にYAMLも処理できるようです。

# 1. 配列の先頭の要素
jq '.[0]'
# 2. id=101を持つnameの値
jq '.[] | select(.id == 101) | .name'
# 3. tagsにwhiteが含まれる要素のidの値
jq '.[] | select(.tags[] == "white") | .id'

JSONPath

https://goessner.net/articles/JsonPath/

XPathのJSON版、とのことです。初出は2007年2月?

JSONPathをCLIで確認するには jsonpath-cli というものが使えそうだったのでこれで試してみます。

# 1. 配列の先頭の要素
jpath '$[0]'
# 2. id=101を持つnameの値
jpath '$[?(@.id==101)].name'
# 3. tagsにwhiteが含まれる要素のidの値
jpath '$[?(@.tags.indexOf("white") != -1)].id'

indexOf は標準のSyntaxには見当たらなかったので拡張機能かもしれません。

JMESPath

https://jmespath.org/

AWSやAzureのCLIにて --query オプションで指定できる形式、というとわかりやすいでしょうか。
サイトのcopyright的には2014年頃にできたもののようです。

jp という公式CLIツールがあったのでこれを使います。

# 1. 配列の先頭の要素
jp '[0]'
# 2. id=101を持つnameの値
jp '[?id==`101`].name'
# 3. tagsにwhiteが含まれる要素のidの値
jp "[?contains(tags, 'white')].id"

数値をバッククォートで括ったり、文字列をシングルクォートで括ったりと、ちょっと分かりにくかったです。。
jq と同じく、パイプが使えて処理を連結できるのは良いですね。

JSON Pointer

https://datatracker.ietf.org/doc/html/rfc6901

RFC6901 で定義された仕様です。(2013年4月)
たとえば、 Open API Specification の $ref で使用されていたりします。

いくつかCLIツールありましたが、ここでは json-joy というのを使ってみます。

# 1. 配列の先頭の要素
json-pointer "/0"
# 2. id=101を持つnameの値
N/A
# 3. tagsにwhiteが含まれる要素のidの値
N/A

JSONの特定位置を指すのが目的のようで、JSONの中からフィルター的に探すようなクエリを書くことはできなさそうでした。(もしあったらごめんなさい)
また、クエリの中に ~ または / がある場合のエスケープが ~0 , ~1 というのもやや分かりにくいですね。

マージ

複数のJSONを結合したり、部分的に差し替えたりするようなことがしたい場合にはこんな選択肢があります。

jq (yq)

前述の jq はJSONのマージもできます。

$ echo '{"a": { "aa": 0 }}' > 1.json
$ echo '{"a": { "ab": 1 }, "b": "hoge"}' > 2.json
$ jq -s add 1.json 2.json
{
  "a": {
    "ab": 1
  },
  "b": "hoge"
}

add コマンドでマージできます。1.json の a の値が消失してる

$ jq -s '.[0] * .[1]' 1.json 2.json
{
  "a": {
    "aa": 0,
    "ab": 1
  },
  "b": "hoge"
}

こういうことも出来るみたい。こちらはちゃんと同名フィールドがマージされてますね。

jsonnet

https://jsonnet.org/

変数や関数を使えるようにしたJSON拡張という位置付けのテンプレート言語だそうです。
JSONをベースにしたような記法ですが、出力はJSON以外にも対応しているみたい。(TOMLやYAML、XMLなど)

$ jsonnet -e '{a: { "aa": 0 }} + {a: { "ab": 1 }, b: "hoge"}'
{
   "a": {
      "ab": 1
   },
   "b": "hoge"
}

+ でマージができるようです。(※オブジェクトに同名のフィールドがあった場合、フィールドはマージされず右辺が勝つ)

JSON Patch

https://datatracker.ietf.org/doc/html/rfc6902

JSON Pointer と一緒に公開された感じの JSON Patch は JSON を書き換えたりする目的の仕様です。HTTPのPATCHメソッドで使用することを想定しているようです。
JSON Patch自体がJSON形式で記述されます。
操作内容は add, replace, remove, move, copy, test から選びます。

JSON Patch も上の方で使用した json-joy で試すことができます。

$ echo '{"a": { "aa": 0 }}' | json-patch '[{"op": "add", "path": "/a/ab", "value": 1}, {"op": "add", "path": "/b", "value": "hoge"}]'
{
    "a": {
        "aa": 0,
        "ab": 1
    },
    "b": "hoge"
}

元のJSON {"a": { "aa": 0 }} に対して、操作 op と位置 path 、値 value を指定することでPATCHを当てる感覚で操作できるんですね。なるほど、シンプルで分かりやすい。

変換

JSONからYAML、XMLなど異なるフォーマットに変換したい場合。

yq

YAMLからJSON、あるいはJSONからYAMLに変換したいだけなら一番シンプルに実現できそうです。

$ a.yaml | yq .
$ b.json | yq -y .

jsonnet

jsonnet の場合は、付属の Standard Library を使う感じで実現します。

$ jsonnet -S -e 'std.manifestYamlDoc({a: { "aa": 0 }} + {a: { "ab": 1 }, b: "hoge"})'
"a":
  "ab": 1
"b": "hoge"

その他

JSON5

https://json5.org/

ゆるい仕様のJSON拡張。 コメントとか書ける。

jo

https://jpmens.net/2016/03/05/a-shell-command-to-create-json-jo/

シェルコマンドでjsonを生成するためのツール

$ jo time=$(date +%s) dir=$HOME
{"time":1457195712,"dir":"/Users/jpm"}

gron

https://github.com/tomnomnom/gron

jsonデータをgrepしやすくするためのツール

$ echo '{"a": {"ab": 1}, "b": "hoge"}' | gron
json = {};
json.a = {};
json.a.ab = 1;
json.b = "hoge";

こんな感じで各要素をキーバリュー形式で表示してくれるので特定キーを含むの部分だけgrepできたりする。

$ echo '{"a": {"ab": 1}, "b": "hoge"}' | gron | grep ab
json.a.ab = 1;

このキーバリュー形式はjson形式に戻すことも可能。組み合わせるとgrepの結果だけjsonにしたりもできる。

$ echo '{"a": {"ab": 1}, "b": "hoge"}' | gron | grep ab | gron -u
{
  "a": {
    "ab": 1
  }
}

参考