JMESPath 是一種 JSON 的查詢語言,讓使用者可以透過簡單的語法,來查詢 JSON 格式的資料,甚至重新組合 JSON,如果運用得當可以幫助我們處理查詢複雜的 JSON 資料,甚至可以簡化程式碼、改善可讀性,這篇文章將帶你輕鬆上手 JMESPath。

支援語言

JMESPath 本身是種查詢語言的規格,不是特定程式語言的函示庫,不過 JMESPath 官方實作了各種程式語言的函示庫,目前支援:Python、Go、Lua、Javascript、PHP、Ruby、Rust。

除了官方的實作之外,社群也根據JMESPath 規格實作了以下語言的函示庫:C++、C++、Elixir、Java、.NETTypeScript

另外,每種程式語言會有自己的使用方式、呼叫方法,詳細請自行參考各語言的實作,這裡我們主要以 JMESPath 原生地的使用方式做介紹。

完整的支援列表請參考官方網站 JMESPath Libraries

快速上手

這裡我提供一個快速認識的切入點,幫助你快速理解 JMESPath 的使用語法。

JMESPath 要查詢的資料來源就是 JSON 格式,我們知道 JSON 本身主要會有兩種資料結構:物件(Object)和陣列(Array),因此我們主要可以利用 JMESPath 對 JSON 做兩種事:

  1. 從物件中取得或計算我們關注的屬性值
  2. 從陣列中執行查詢條件

這兩種資料結構,或者說對這兩種目的,分別有各自的處理函數和存取語法。

例如,我們要取值,就可以使用 . 來存取物件的屬性,例如 location.name 可以取得 location 物件中的 name 屬性值。

如果要查詢陣列,可以使用 [] 搭配函數來查詢陣列中的元素,例如 locations[?contains(name == 'Seattle')] 可以取得 locations 陣列中,那些物件的 name 屬性是 Seattle 的元素。

你可以這樣想,通常我們會用 . 來取得物件的屬性值,用 [] 來取得陣列資料,而在取得陣列資料的時候,通常會搭配一些查詢語法,進行資料過濾的動作,因此查詢的語法就是在 [] 中,並以 ? 開頭來接收要查詢的條件。

基本上 JMESPath 的語法就這兩個方向,剩下的就是各種運算符、運算式、函數的組合和搭配,最終透過所組合的表達式來取得我們想要的 JSON 資料樣貌。

JMESPath 官網的 Tutorial 提供了許多範例可以參考。

範例:基本表達式 Basic Expressions

有以下 JSON 資料,要取得 locationname 屬性值,可以使用 location.name 表達式。

{
    "location": {
        "name": "Seattle",
        "state": "WA"
    }
}

有以下 JSON 資料,要取得 locations 清單中的第 1 筆物件 (Seattle),可以使用 locations[0] 表達式;要取得倒數第 2 筆物件 (New York),可以使用 locations[-2] 表達式。索引值從 0 開始,負數則表示倒數,當查查無資料則會回傳 null

{
    "locations": [
        {"name": "Seattle", "state": "WA"},
        {"name": "New York", "state": "NY"},
        {"name": "Bellevue", "state": "WA"}
    ]
}

如果 JSON 資料本身就是陣列,如以下 JSON 資料,可以直接使用 [] 來查詢,例如要取得清單中第 2 筆資料,可以使用 [1] 表達式,這樣會取得 {"name": "New York", "state": "NY"}

[
    {"name": "Seattle", "state": "WA"},
    {"name": "New York", "state": "NY"},
    {"name": "Bellevue", "state": "WA"}
]

範例:切片表達式 Slicing Expressions

這裡我們統一有以下 JSON 資料:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

切片表達式是用來取得陣列中的一部分,主要會使用到 : 運算符,然後搭配索引值。

取得指定區間的資料,例如取得 3 到 6 的資料,可以使用 [3:6] 表達式。請注意,這裡的索引值是從 0 開始,所以 [3:6] 會取得 3, 4, 5 這三個元素。

這裡的表達式就是 [start:stop] 的意思,從哪裡開始(包含)到哪裡結束(不包含)。

如果 start 沒有給值則代表從第 1 個開始取值,因此要取得前 3 個元素,可以使用 [0:3] 表達式,也可以使用 [:3] 表達式。如果 stop 沒有給值則代表會取到最後一個元素。

在切片表達式中還可以設定 step,也就是取值的間隔,表達式就是 [start:stop:step],例如 [::2] 會取得所有元素,但是間隔 2 個取值,所以會取得 0, 2, 4, 6, 8 這幾個元素。

範例:投影表達式 Projection Expressions

這個表達式非常常用,主要用來將物件轉換成另一種形式,例如將陣列中的某物件屬性值取出來成為清單,或是將取得的值指派給新的屬性,組合成新物件。

這裡我們統一有以下 JSON 資料:

{
    "locations": [
        {"name": "Seattle", "state": "WA"},
        {"name": "New York", "state": "NY"},
        {"name": "Bellevue", "state": "WA"}
    ]
}

如果想要將資料轉換成 { "longName":"" } 這樣的資料樣貌,可以使用 locations[*].{longName:name} 表達式,前段 locations[*] 用於取得所有 locations 陣列中的資料,後段 .{longName:name} 則是將原本的 name 屬性值值派給 longName 這個新屬性值,結果如下:

[
    {"longName": "Seattle"},
    {"longName": "New York"},
    {"longName": "Bellevue"}
]

這裡做兩個延伸,如果不想要指定新的屬性名稱,可以直接使用 locations[*].name 表達式,這樣會取得 locations 陣列中所有物件的 name 屬性值,並轉換成一個陣列,結果如下:

["Seattle", "New York", "Bellevue"]

另一個延伸,上面使用 locations[*] 取得所有 locations 陣列中的資料,這裡的 * 我們可替換成條件表達式,例如 locations[?state == 'WA'] 可以取得 locations 陣列中,那些物件的 state 屬性是 WA 的元素,結果如下:

[
    {"name": "Seattle", "state": "WA"},
    {"name": "Bellevue", "state": "WA"}
]

比較運算子清單如下:

比較運算子 範例
== locations[?state == ‘WA’]
> people[?age > 18]
< people[?age < 20]
|| people[?age == 18 || age == 20]
&& locations[?name == ‘Seattle’ && state == ‘WA’]

除了用條件表達式之外,也可以搭配內建的函數來做條件判斷,例如 locations[?contains(name, 'a')] 可以取得 locations 陣列中,那些物件的 name 屬性值包含 e 的元素。

JMESPath 提供非常多內建的函數,可以根據你的需要做各種條件判斷。

範例:管線表達式 (Pipe Expression)

這也是一個非常重要且非常強大的表達式,這裡的 Pipe 概念跟 Unix 的管線概念很像,可以將前一個表達式的結果當作下一個表達式的輸入,這樣可以將多個表達式串接在一起,達到更複雜的查詢結果。

舉一個簡單的例子,假設我們有以下 JSON 資料:

{
    "locations": [
        {"name": "Seattle", "state": "WA"},
        {"name": "New York", "state": "NY"},
        {"name": "Bellevue", "state": "WA"},
        {"name": "Olympia", "state": "WA"}
    ]
}

我想要取得 locations 陣列中,那些物件的 state 屬性是 WA 的元素,並且只取得 name 屬性值,然後最終結果只需要前 2 筆資料即可,這時可以使用 locations[?state == 'WA'].name | [:2] 表達式,結果如下:

["Seattle", "Bellevue"]

如果要先做字母排序再取資料了話,表達式可以寫成 locations[?state == 'WA'].name | sort(@) | [:2],中間先使用 sort(@) 這個內建函數將 name 屬性值做排序,再取前 2 筆資料。

藉由管線表達式,我們就可以組合出更複雜的查詢效果。

範例:巢狀陣列過濾

這個範例比較複雜一點,假設我們有以下 JSON 資料:

{
    "locations": [
        { "name": "Seattle", "list": [{ "id": "1" }, { "id": "3" }] },
        { "name": "New York", "list": [{ "id": "2" }, { "id": "3" }] },
        { "name": "Bellevue", "list": [{ "id": "2" }, { "id": "3" }] },
        { "name": "Olympia", "list": [{ "id": "1" }, { "id": "2" }] }
    ]
}

期望在這個巢狀陣列中針對 list 做過濾的動作,例如取得 locations 陣列中,那些物件的 list 陣列中有 id 屬性值是 1 的元素,這時可以使用 locations[?list[?id == '1']] 表達式,結果如下:

[
    {
        "name": "Seattle",
        "list": [
            { "id": "1" },
            { "id": "3" }
        ]
    },
    {
        "name": "Olympia",
        "list": [
            { "id": "1" },
            { "id": "2" }
        ]
    }
]

接著想要列出 list 底下的物件,並且只取得 id 屬性值是 1 的元素,這時可以改寫成 locations[?list[?id == '1']].list[] | [?contains(id,'1')] 表達式,透過管線的處理,將所要的值過濾出來,結果如下:

[
    { "id": "1" },
    { "id": "1" }
]

看到最後的輸出,你可能會想要把重複的資料作合併,可惜的是 JMESPath 本身並沒有提供這樣的功能,因此這樣的操作會需要透過程式做額外的處理。

後記

讀完這篇文章之後,基本上大部分的 JMESPath 表達式應該都能應付,如果有更複雜的需求,可以參考 JMESPath 官方網站提供的規格內建函數,特別是內建函數的部分,可以先掃過一遍,當實務上有需要的時候,再去文件找怎麼使用。

另外,在使用 Azure CLI 來查詢 Azure DevOps Pipeline 資訊的時候,可以透過 --query 參數並搭配 JMESPath 來做資料過濾,這樣可以讓你的查詢更精準,加快找到指定目標的作業。

JMESPath Playground

Data

Expression

Result



參考資料:


Poy Chang

Trial and Error