こちらの記事を見て「自分も最近似たようなことやったなぁ」と思ったので、他の方法も含めてメモがてら書いておきます。
なお、扱うJSONデータについては、上記記事と同じものを使うことにします。
--raw-output/-rオプションを使う方法
まずは記事と同じやりかた。
-rで結果の文字列をJSONエンコードされていない生の文字列として出力して、それをさらにjqでフォーマットする方法。
cat message.json | jq -r '.Cause' | jq .
{
"errorMessage": "An error occurred (InvalidElasticIpFault) when calling the RestoreFromClusterSnapshot operation: The Elastic IP address aa.bbb.ccc.ddd is already associated with another instance.",
"errorType": "InvalidElasticIpFault",
"stackTrace": [
" 省略\n",
" File \"/var/runtime/botocore/client.py\", line 316, in _api_call\n return self._make_api_call(operation_name, kwargs)\n",
" File \"/var/runtime/botocore/client.py\", line 635, in _make_api_call\n raise error_class(parsed_response, operation_name)\n"
]
}
ちなみにjq -rの時点ではこういう結果になります。
cat message.json | jq -r '.Cause'
{"errorMessage": "An error occurred (InvalidElasticIpFault) when calling the RestoreFromClusterSnapshot operation: The Elastic IP address aa.bbb.ccc.ddd is already associated with another instance.", "errorType": "InvalidElasticIpFault", "stackTrace": [" 省略\n", " File \"/var/runtime/botocore/client.py\", line 316, in _api_call\n return self._make_api_call(operation_name, kwargs)\n", " File \"/var/runtime/botocore/client.py\", line 635, in _make_api_call\n raise error_class(parsed_response, operation_name)\n"]}
つまり、Causeの中身をそのまま文字列として取り出せば、それがちょうどJSONなので、続けてjq .にかけられるということですね。
fromjsonを使う方法
tojson, fromjsonというフィルタを使うと、JSONエンコードしたりJSONデコードすることができます。
fromjsonを目的の.Causeにかけることで文字列からJSONに展開した結果が得られます。
cat message.json | jq '.Cause | fromjson'
{
"errorMessage": "An error occurred (InvalidElasticIpFault) when calling the RestoreFromClusterSnapshot operation: The Elastic IP address aa.bbb.ccc.ddd is already associated with another instance.",
"errorType": "InvalidElasticIpFault",
"stackTrace": [
" 省略\n",
" File \"/var/runtime/botocore/client.py\", line 316, in _api_call\n return self._make_api_call(operation_name, kwargs)\n",
" File \"/var/runtime/botocore/client.py\", line 635, in _make_api_call\n raise error_class(parsed_response, operation_name)\n"
]
}
おまけ: 全体を維持したままCauseだけ展開する
|=(update-assignment)を使うと、任意の部分を差し替えることができます。
これとfromjsonを組み合わせることで、Causeの部分をfromjsonで展開したものに差し替えたJSON全体が得られます。
cat message.json | jq '.Cause |= fromjson'
{
"Error": "InvalidElasticIpFault",
"Cause": {
"errorMessage": "An error occurred (InvalidElasticIpFault) when calling the RestoreFromClusterSnapshot operation: The Elastic IP address aa.bbb.ccc.ddd is already associated with another instance.",
"errorType": "InvalidElasticIpFault",
"stackTrace": [
" 省略\n",
" File \"/var/runtime/botocore/client.py\", line 316, in _api_call\n return self._make_api_call(operation_name, kwargs)\n",
" File \"/var/runtime/botocore/client.py\", line 635, in _make_api_call\n raise error_class(parsed_response, operation_name)\n"
]
}
}
おわりに
JSONの中に文字列としてJSONを入れるなんておかしなことだ、最初からJSONの一部として入れておけばいいのに、と思う人もいるかもしれませんが、さまざまな理由によってこうなるケースが時々あるのです。
- 任意のデータが入る箇所にたまたまJSONが入るケース
- 内容は定義されていなくて、任意のデータが入れられる
- 何らかの処理結果や別のシステムなどから得られたデータをそのまま詰めるケース
- 様々な種類のJSONが入るが、型を定義しづらい、インターフェース的には感知したくないケース
- その場合、中身を扱うレイヤーではJSON Schema等を使って妥当性を検証する
- 別のフォーマットだったものをJSONに変換したケース
などなど
こういった厄介なJSONと向き合うときは、jqのマニュアルを一読してみることをおすすめします。
jqには本当にいろいろな機能が搭載されているので、マニュアルに目を通してみると新しい発見があるかもしれません。