JMESPath チュートリアルでプロジェクションを理解する

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

渡辺です。

前回に引き続き、AWSCLIのqueryオプションで利用できるJMESPathのチュートリアルを紹介します。 今回のチュートリアルを終わらせると、かなり細かい抽出まで可能になるのでしょう。

プロジェクション(投射)

プロジェクション(投射)は、JMESPathのキーとなる機能のひとつです。 イメージしずらいですが、要素をイイ感じにArrayに変換していくことができます。

リストのプロジェクション

ワイルドカードを使った[*]はJSONのArrayに投射します。

[
    {"first": "James", "last": "d"},
    {"first": "Jacob", "last": "e"},
    {"missing": "different"},
    null
]
JMESPath Result
[*] [ {"first": "James", "last": "d"}, {"first": "Jacob", "last": "e"}, {"missing": "different"}]
[*].first ["James", "Jacob", "Jayden"]

最もシンプルな[*]はArrayをArrayに投射します。 この時、nullは除外されることがひとつの特徴です。

また、[*].firstのように、オブジェクトのキーで子要素を指定すると、Arrayの各オブジェクトの値を抽出できます。 この時、nullとキーが存在しないオブジェクトは除外されます。

対象がオブジェクトの場合、people[*]のようにキーを前に付与して、対象のArrayを指定します。

{
  "people": [
    {"first": "James", "last": "d"},
    {"first": "Jacob", "last": "e"},
    {"first": "Jayden", "last": "f"},
    null,
    {"missing": "different"}
  ],
  "foo": {"bar": "baz"}
}
JMESPath Result
people[*].first [ {"first": "James", "last": "d"}, {"first": "Jacob", "last": "e"}, {"missing": "different"}]
people[*].first ["James", "Jacob", "Jayden"]

Arrayの各オブジェクトから特定の要素を列挙する基本的な使い方となるでしょう。

スライスとリストのプロジェクション

ワイルドカードを使った[*]はArray全体を投射します。 ワイルドカードの代わりにスライスを利用するとArrayの一部を投射できます。

{
  "people": [
    {"first": "James", "last": "d"},
    {"first": "Jacob", "last": "e"},
    {"first": "Jayden", "last": "f"},
    {"missing": "different"}
  ],
  "foo": {"bar": "baz"}
}
JMESPath Result
people[0:1].first ["James", "Jacob"]
people[::-1].first ["Jayden", "Jacob", "James"]

スライスされるタイミングに注意しましょう。

{
  "people": [
    null,
    {"first": "James", "last": "d"},
    {"first": "Jacob", "last": "e"},
    {"first": "Jayden", "last": "f"},
    {"missing": "different"}
  ],
  "foo": {"bar": "baz"}
}
JMESPath Result
people[0:2].first ["James"]

この場合、people[0:2]でスライスされた [null, {"first": "James", "last": "d"}] に対して、投射が行われることになります。 投射のタイミングでnullが除去されるので、期待される結果を得られていません。

オブジェクトのプロジェクション

*は、オブジェクトの値のみをArrayに投射します。 キーには興味は無く、各値にのみ興味がある時に有効です。

{
  "ops": {
    "functionA": {"numArgs": 2},
    "functionB": {"numArgs": 3},
    "functionC": {"variadic": true}
  }
}
JMESPath Result
ops.* [1, 2, 3]

opsオブジェクトの各値がArrayとして抽出されています。

{
  "ops": {
    "functionA": {"numArgs": 2},
    "functionB": {"numArgs": 3},
    "functionC": {"variadic": true}
  }
}
JMESPath Result
ops.*.numArgs [2, 3]

ops.*.numArgsのように子要素を指定することができます。 キーが存在しない場合には除外される挙動も同様です。

フラット化のプロジェクション

JMESPathで複雑なJSONにプロジェクションを繰り返していくと、往々にしてネストしたArrayが作られます。

{
  "reservations": [
    {
      "instances": [
        {"state": "running"},
        {"state": "stopped"}
      ]
    },
    {
      "instances": [
        {"state": "terminated"},
        {"state": "runnning"}
      ]
    }
  ]
}
JMESPath Result
reservations[].instances[].state [["running", "stopped"], ["terminated", "runnning"]]

ちょっとイケていない結果ですね・・・。 ここでArrayをフラット化するには、[]を利用します。

[
  [
    "running",
    "stopped"
  ],
  [
    "terminated",
    "runnning"
  ]
]
JMESPath Result
[] ["running", "stopped", "terminated", "runnning"]

投射されたArrayに投射を繰り返すイメージが掴めれば、最初のJSONが次のように出力されることが解ると思います。

JMESPath Result
reservations[].instances[].state [["running", "stopped"], ["terminated", "runnning"]]
reservations[].instances[].state[] ["running", "stopped", "terminated", "runnning"]
reservations[*].instances[].state ["running", "stopped", "terminated", "runnning"]
reservations[].instances[*].state [["running", "stopped"], ["terminated", "runnning"]]

実際にクエリを書く場合は、1段階づつ投射すると欲しい結果にたどり着くことができます。

プロジェクション時のフィルタ

[]の中には様々なフィルタを埋め込むことで、投射するArrayをコントロールできます。

{
    "Reservations": [
        {
            "Instances": [
                {
                    "State": {
                        "Name": "running"
                    },
                    "InstanceType": "m3.medium",
                    "InstanceId": "i-001"
                },
                {
                    "State": {
                        "Name": "stopped"
                    },
                    "InstanceType": "t2.micro",
                    "InstanceId": "i-002"
                }
            ]
        },
        {
            "Instances": [
                {
                    "State": {
                        "Name": "terminated"
                    },
                    "InstanceType": "m3.medium",
                    "InstanceId": "i-003"
                },
                {
                    "State": {
                        "Name": "running"
                    },
                    "InstanceType": "t2.small",
                    "InstanceId": "i-004"
                }
            ]
        }
    ]       
}

インスタンス一覧からrunningのインスタンスIDだけ抽出したい感じです。

JMESPath Result
Reservations[].Instances[].InstanceId[] ["i-001", "i-002", "i-003", "i-004"]
Reservations[*].Instances[?State.Name=='running'].InstanceId[] ["i-001", "i-004"]

?State.Name=='running'でArrayをフィルタリングしています。 最後の[]はフラット化ですよ。

パイプ

JMESPathのアウトプットはJSONオブジェクト(Array)です。 |(パイプ)を利用すれば、JMESPathの出力を次の式の入力に渡すことができます。

JMESPath Result
Reservations[*].Instances[?State.Name=='running'].InstanceId[] [0] | "i-001"

基本的にはプロジェクションを繰り返していけばいいので、使い所は今の所は見えていません。 なお、可読性は良いです。

マルチセレクト

JMESPathでは複数項目を抽出する場合、[A, B]といった書式を指定します。

JMESPath Result
Reservations[*].Instances[?State.Name=='running'].[InstanceId, InstanceType] [[[ "i-001", "m3.medium" ] ], [["i-004", "t2.small" ]]]
Reservations[*].Instances[?State.Name=='running'].[InstanceId, InstanceType][] [[ "i-001", "m3.medium" ] ], [["i-004", "t2.small" ]]

イイ感じになってきました。

でも、Arrayではなくオブジェクトとして欲しいんじゃ…と思いますね。 [要素, 要素]は配列を返しますが、 {キー名:要素名, キー名:要素名} とすることでオブジェクトを作成します。

JMESPath Result
Reservations[*].Instances[?State.Name=='running'].{Id:InstanceId, Type:InstanceType}[] [{"Id": "i-001", "Type": "m3.medium" }, { "Id": "i-004", "Type": "t2.small" } ]

巨大なJSONレスポンスを適度な大きさで使いやすい形に加工できるようになりました。

まとめ

JMESPathは取っつきにくい部分がありますが、プロジェクションの感覚を掴むと一気に使いやすくなります。 大きなJSONオブジェクトを少しずつ求める形に加工していくことができるため、AWSCLIをより使いやすく使えるようになります。

早速、runningのEC2インスタンスについて、インスタンスIDとインスタンスタイプを抽出してみましょう。

aws ec2 describe-instances --query "Reservations[*].Instances[?State.Name=='running'].{Id:InstanceId, Type:InstanceType}[]"