JSON内に文字列として埋め込まれたJSONをjqで展開する

こちらの記事を見て「自分も最近似たようなことやったなぁ」と思ったので、**他の方法も含めて**メモがてら書いておきます。

- [JSONの中に文字列として記載されたJSONの値をjqで扱ってみる | Developers.IO](https://dev.classmethod.jp/articles/json-in-json-string-use-jq/)

なお、扱うJSONデータについては、上記記事と同じものを使うことにします。

## `--raw-output`/`-r`オプションを使う方法

まずは記事と同じやりかた。

`-r`で結果の文字列をJSONエンコードされていない生の文字列として出力して、それをさらに`jq`でフォーマットする方法。

```sh
cat message.json | jq -r '.Cause' | jq .
```

```json
{
  "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`の時点ではこういう結果になります。

```sh
cat message.json | jq -r '.Cause'
```

```json
{"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に展開した結果が得られます。

```sh
cat message.json | jq '.Cause | fromjson'
```

```json
{
  "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全体が得られます。

```sh
cat message.json | jq '.Cause |= fromjson'
```

```json
{
  "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には本当にいろいろな機能が搭載されているので、マニュアルに目を通してみると新しい発見があるかもしれません。

## 参考

- [jq Manual (development version)](https://stedolan.github.io/jq/manual/)