小技

日々の仕事の中で調べたり発見したりしたことを書き留めていくブログ

jq で JSON オブジェクトのキーをソートする

複数の JSON データを作成する中で、途中でオブジェクトのキーの並びが変わってしまった!

$ cat sample_a.json
{
    "key0": "a0",
    "key1": "a1",
    "key2": "a2"
}
$ cat sample_b.json
{
    "key0": "b0",
    "key2": "b2",
    "key1": "b1"
}

そんなことが実際に起こり得るのかわからないですが、この場合まとめて CSV として出力 しようとすると、行によって並びが異なる状態のまま出力されてしまいます。

$ jq . *.json
{
  "key0": "a0",
  "key1": "a1",
  "key2": "a2"
}
{
  "key0": "b0",
  "key2": "b2",
  "key1": "b1"
}
$ jq -r '[.[]] | @csv'  *.json
"a0","a1","a2"
"b0","b2","b1"

キーの並びを指定する

キーの並びを指定してオブジェクトを作り直すと、いずれの行でも同じ並び順で出力することができます。 下記のように {} の中にキーを列挙することで実現できます。

$ jq '{key0, key1, key2}' *.json
{
  "key0": "a0",
  "key1": "a1",
  "key2": "a2"
}
{
  "key0": "b0",
  "key1": "b1",
  "key2": "b2"
}
$ jq -r '{key0, key1, key2} | [.[]] | @csv' *.json
"a0","a1","a2"
"b0","b1","b2"

なお、オブジェクトを作るときは {<key>: <value>, ...} と書くものですが、元のオブジェクトからキー名そのままで取り出す場合は、上記のようにショートカットが使えるようです。

(jq のマニュアルより)

if the input is an object with "user", "title", "id", and "content" fields and you just want "user" and "title", you can write

{user: .user, title: .title}
Because that is so common, there's a shortcut syntax for it: {user, title}.

オブジェクトのキーをソートする

サンプルだとキーの数が連番で3つだけなので列挙するのも簡単ですが、数が多くなるとショートカットを使っても大変そうです。 to_entries でオブジェクトをキーと値の組み合わせの配列に変換することで、ソートができるようになります。

$ jq 'to_entries' *.json
[
  {
    "key": "key0",
    "value": "a0"
  },
  {
    "key": "key1",
    "value": "a1"
  },
  {
    "key": "key2",
    "value": "a2"
  }
]
[
  {
    "key": "key0",
    "value": "b0"
  },
  {
    "key": "key2",
    "value": "b2"
  },
  {
    "key": "key1",
    "value": "b1"
  }
]

sort_by でキーをもとにソートした後、from_entries で再度オブジェクトに戻せます。

$ jq 'to_entries | sort_by(.key) | from_entries' *.json
{
  "key0": "a0",
  "key1": "a1",
  "key2": "a2"
}
{
  "key0": "b0",
  "key1": "b1",
  "key2": "b2"
}

sort_by の部分を工夫してやることで、キー名以外でのソートもできるようです。

jq の --sort-keys(-S) オプションを使う

長々と書いてきましたが、jq には -S(--sort-keys) というオプションがあり、オブジェクトのキーをソートした状態で出力することができます。

(jq のマニュアルより)

--sort-keys / -S:
Output the fields of each object with the keys in sorted order.

$ jq --sort-keys . *.json
{
  "key0": "a0",
  "key1": "a1",
  "key2": "a2"
}
{
  "key0": "b0",
  "key1": "b1",
  "key2": "b2"
}

これを使っておけば万事解決と思いきや、そう簡単には行きません。

$ jq --sort-keys -r '[.[]]' *.json
[
  "a0",
  "a1",
  "a2"
]
[
  "b0",
  "b2",
  "b1"
]

どうも、オブジェクトのキーのソートは最終的な出力に対してのみ行われるように思えます。 一旦オブジェクトのキーをソートして出力し、パイプで再度 jq コマンドに入力として渡してやる必要があります。

$ jq --sort-keys . *.json | jq -r '[.[]]'
[
  "a0",
  "a1",
  "a2"
]
[
  "b0",
  "b1",
  "b2"
]

参考