programming

JSONPathとJMESPathとjq

とりあえずこの3つを全部使いこなせたらかっこよくね?

jsonパーサー多すぎ問題

インフラとかクラウドやってるとコマンドでjsonをチョキチョキする機会はそれなりに多いと思うのだけど、ツールによって組み込みのパーサーが違うっていう問題がある。

たとえばAzure CLIだとJMESPathで、これはPython書く人にとってはわりと分かりやすくて好き。

とはいえ、いろんな場面で使うことも考えると一般的にはbash環境なら、jqがデファクトだろうか。

ただ、jqはたとえばこのページとかでも言及されてるけど、インストールが必要なので環境によっては使えなかったりする問題がある。

また、KubernetesをやるならJSONPathがネイティブサポートのため、これも覚えておいたほうがよい。

したがって、少なくともJMESPathとjqとJSONPathの3種類をある程度は使えないと、そもそもクラウド人材として人権がないということになる。

jsonの基本

押さえておくべきは、jsonにおける配列オブジェクトの違いのみだ。

配列はいわずもがな。オブジェクトというのはいわゆるkey-valueで値を格納しているdicみたいなやつを指す。

// 配列
["hoge", "fuga", "piyo"]

// オブジェクト
{"hoge": "1", "fuga": "2", "piyo": "3"}

環境の準備

ローカルに環境を準備して試してみよう。

JSONPathのインストール

pip3 install git+https://github.com/mclarkson/JSONPath.sh#egg=JSONPath.sh
$ echo '{"hoge":"100"}' | JSONPath.sh -b hoge
100

jqのインストール

sudo apt upgrade && sudo apt install jq
$ echo '{"hoge":"100"}' | jq .hoge
"100"

JMESPathのインストール

sudo wget https://github.com/jmespath/jp/releases/download/0.1.2/jp-linux-amd64 -O /usr/local/bin/jp && sudo chmod +x /usr/local/bin/jp
$ echo '{"hoge": "100"}' | jp hoge
"100"

取得クエリの比較

環境の準備が出来たので、さっそく試し打ちをしていく。

試し打ち用のjsonの準備

ネットに公開されているJSONPathのplaygroundからサンプルを引っ張ってこよう。

{
  "firstName": "John",
  "lastName": "doe",
  "age": 26,
  "address": {
    "streetAddress": "naist street",
    "city": "Nara",
    "postalCode": "630-0192"
  },
  "phoneNumbers": [
    {
      "type": "iPhone",
      "number": "0123-4567-8888"
    },
    {
      "type": "home",
      "number": "0123-4567-8910"
    }
  ]
}

test.jsonとでも名前を付けて保存しておこう。 以下では、説明に適した部分を適当に抜き出して使うことにする。

オブジェクト要素の取得

jqの場合はトップレベルの要素にもピリオド.が必要。 JSONPathは付けても付けなくてもOK。 JMESPathは付けるとエラーになる。

# json
$ cat test.json
{"firstName": "John", "lastName" : "Doe"}

# JMESPath
$ cat test.json | jp firstName
"Jhon"

# jq
$ cat test.json | jq .firstName
"Jhon"

# JSONPath
$ cat test.json | JSONPath.sh -b firstName
Jhon

配列要素の取得

配列の場合もピリオド.のルールは同じ。

# json
$ cat test.json
["John", "doe"]

# JMESPath
$ cat test.json | jp [0]
"Jhon"

# jq
$ cat test.json | jq .[0]
"Jhon"

# JSONPath
$ cat test.json | JSONPath.sh -b [0]
Jhon

チェーン

ピリオド.でチェーンが可能。

# json
$ cat test.json
{
  "firstName": "John",
  "lastName": "doe",
  "phoneNumbers": [
    {
      "type": "iPhone",
      "number": "090-000-0000"
    },
    {
      "type": "home",
      "number": "000-0000-0000"
    }
  ]
}

# JMESPath
$ cat test.json | jp phoneNumbers[0].type
"iPhone"

# jq
$ cat test.json | jq .phoneNumbers[0].type
"iPhone"

# JSONPath
$ cat test.json | JSONPath.sh -b phoneNumbers[0].type
iPhone

配列のスライス

おなじみの[s, f)の半開区間で指定する。
jqは配列だけでなく文字列もスライスできるが、[1:4:2]のようなstepの指定はできない。

# JMESPath
$ echo '["a", "b", "c", "d", "e"]' | jp [1:4]
[
  "b",
  "c",
  "d"
]

# jq
$ echo '["a", "b", "c", "d", "e"]' | jq .[1:4]
[
  "b",
  "c",
  "d"
]

# JSONPath
$ echo '["a", "b", "c", "d", "e"]' | JSONPath.sh -j [1:4]
[
  "b",
  "c",
  "d"
]

# jq(str)
$ echo '{"foo": "bbarr"}' | jq .foo[1:4]
"bar"

playground

各パーサーのplaygroundも紹介しておく。

発展

さすがにこれだけだと心許ないので、もう少し実践的な例も。

オブジェクトの配列から共通のkeyで抜き出す

下記のjsonからfirstに対応する値だけを抜き出したいケースはよくありそうだ。

{
  "people": [
    { "first": "James", "last": "d" },
    { "first": "Jacob", "last": "e" },
    { "first": "Jayden", "last": "f" },
    { "missing": "different" }
  ],
  "foo": { "bar": "baz" }
}
# JMESPath
people[*].first
*[*].first|[] # |[]を付けないと配列in配列が返る
# JSONPath
people[*].first
$..first # recursiveに探せて便利
# jq
.people[].first|values # valuesを付けないとnullを含む

配列から条件に一致する要素だけを得る

以下の配列から3以上のみを抜き出したいとする。

[1, 2, 3, 4, 5, 6]
# jq
[.[]|select(.>=3)]
# JMESPath
[? @>=`3`]
# JSONPath
(書き方わからん、だれか教えて)

書きかけ

まだまだ多くのパターンや可能性があると思うが、現状必要に迫られているパターンがないので機会に恵まれたときに気が向いたら追記する。