JMESPath สำหรับผู้เริ่มต้น: การคิวรีแบบ Projection ที่เหนือกว่า JSONPath
สรุปสำคัญ: JMESPath ปรับรูปชุดออบเจกต์ได้ตรงไปตรงมากว่า JSONPath คู่มือสำหรับผู้เริ่มต้นนี้ครอบคลุมไวยากรณ์, projection, pipe, ฟังก์ชันในตัว และการใช้งานร่วมกับ AWS CLI

สรุปสั้น: ใช้ JSON Pointer เพื่อระบุตำแหน่งตามตำแหน่ง ใช้ JSONPath เพื่อกรองหรือเรียกซ้ำ และใช้ JMESPath เมื่อต้องปรับรูปผลลัพธ์หรือ project บน command line; ฟิลด์เดียวที่รู้อยู่แล้วไม่ต้องใช้ทั้งสองแบบ
ถ้าเป้าหมายของคุณคือการปรับรูปชุดออบเจกต์ให้เป็นรูปแบบอื่น JMESPath มักตรงไปตรงมากว่า JSONPath มันไม่ได้ค้นหาโหนด แต่สร้างผลลัพธ์ขึ้นมา
1. JMESPath คืออะไร
JMESPath คือภาษาคิวรีที่ออกแบบมาเพื่อ JSON โดยเฉพาะ เดิมถูกสร้างขึ้นเพื่อกรองผลลัพธ์ของ AWS CLI และต่อมาได้แพร่กระจายไปยังหลายภาษาผ่าน implementation อย่าง jmespath.js, jmespath.py และ go-jmespath เป้าหมายของมันต่างจาก JSONPath:
- JSONPath เน้นการเลือกชุดโหนดตามพาธและเงื่อนไข แล้วคืนค่ากลับเป็นชุดโหนด;
- JMESPath เน้นการแปลง JSON ขาเข้า แล้วคืนค่าในรูปร่างใดก็ได้
ความต่างนี้ทำให้ JMESPath โดดเด่นเป็นพิเศษในเรื่อง list projection, การเปลี่ยนชื่อฟิลด์ และการตัดทอนผลลัพธ์ เช่น ลดฟิลด์ยี่สิบฟิลด์จากการตอบกลับของ API ให้เหลือสามฟิลด์ที่ frontend ต้องการจริง ๆ ถ้าคุณยังไม่ได้อ่านพื้นฐานของ JSONPath เริ่มจาก ภาษา JSONPath ในทางปฏิบัติ
2. ไวยากรณ์พื้นฐาน: จาก Identifier ถึง Index
JMESPath ใช้สัญลักษณ์น้อยกว่า JSONPath และอ่านแล้วเหมือนนิพจน์การดึงข้อมูลมากกว่า:
| ไวยากรณ์ | ความหมาย | ตัวอย่าง |
|---|---|---|
name |
เข้าถึงสมาชิกของออบเจกต์ | people |
a.b |
เข้าถึงสมาชิกที่ซ้อนกัน | store.bicycle.color |
[0] |
เข้าถึงดัชนีของ array | people[0] |
[-1] |
เข้าถึงสมาชิกตัวสุดท้าย | people[-1] |
[0:2] |
Slice โดยนับจุดเริ่มและไม่นับจุดสิ้นสุด | people[0:2] |
[*] |
List projection | people[*].name |
[] |
Flatten projection | people[].tags[] |
[?condition] |
Filter projection | people[?age > 18] |
{key: expr} |
Multi-select hash | {name: name, age: age} |
[expr, expr] |
Multi-select list | [name, age] |
| |
ส่งผลลัพธ์ฝั่งซ้ายเข้าไปฝั่งขวา | people[*].age | [0] |
ตัวอย่างเล็ก ๆ:
{
"people": [
{ "name": "Alice", "age": 31, "city": "Shanghai" },
{ "name": "Bob", "age": 17, "city": "Beijing" }
]
}
people[0].name → "Alice"
people[-1].city → "Beijing"
people[0:2].name → ["Alice", "Bob"]
สังเกตว่าทำไม people[0:2].name คืนค่าเป็น array: slice ให้ผลเป็นลิสต์ และ .name ที่ตามมาจะถูก project โดยอัตโนมัติลงบนทุกสมาชิกของลิสต์นั้น นี่คือความต่างที่ใหญ่ที่สุดระหว่าง JMESPath กับการเข้าถึงออบเจกต์แบบปกติ
3. Projection: เปลี่ยนลิสต์หนึ่งให้เป็นอีกลิสต์หนึ่ง
Projection คือแนวคิดหลักของ JMESPath มันทำงานสองขั้น: ประเมินสมาชิกทุกตัวของลิสต์ แล้วรวบรวมผลลัพธ์เป็นลิสต์ใหม่ สมาชิกที่ประเมินแล้วได้ null จะถูกตัดทิ้งอัตโนมัติ
people[*].name // ["Alice", "Bob"]
people[*].{name: name, age: age} // a list of objects
people[?age > 18].name // ["Alice"]
บรรทัดที่สองคือนิพจน์ projection แบบฉบับ: people[*].{name: name, age: age} มันสร้างสมาชิกทุกตัวขึ้นใหม่เป็น { "name": ..., "age": ... } และทิ้งฟิลด์ city ที่เกินมา เมื่อเทียบกับ JSONPath ที่ให้โหนดแล้วคุณต้อง map ในโค้ดต่อ แบบนี้ประหยัดลูปไปหนึ่งรอบ
Projection สองแบบที่ควรแยกให้ออก:
[*]รักษาโครงสร้างลิสต์เดิมไว้และข้ามค่า null;[]เป็น flatten projection ที่ยุบลิสต์ซ้อนหนึ่งชั้นก่อน ซึ่งเหมาะกับนิพจน์อย่างpeople[].tags[]ที่ต้องการ "แท็กทั้งหมดของทุกคน"
4. Pipe และ Multi-Select
Pipe | หมายถึง "หยุด projection ปัจจุบันแล้วถือผลลัพธ์เป็นอินพุตใหม่" การใช้ที่พบบ่อยที่สุดคือการเอาดัชนีหลัง projection:
people[*].name | [0] → "Alice"
people[*].age | sort(@) | [-1] → the maximum age
ถ้าไม่มี pipe, people[*].name[0] จะถูกตีความเป็น "เอาตัวอักษรตัวแรกของทุกชื่อ" ซึ่งไม่ใช่สิ่งที่คุณต้องการ Pipe เป็นสัญลักษณ์ที่ถูกละเลยง่ายที่สุดและมีประโยชน์ที่สุดใน JMESPath
Multi-select มีสองรูปแบบ:
// Multi-select hash: the result is an object
people[0].{name: name, age: age}
// Multi-select list: the result is an array
people[0].[name, age]
Multi-select hash เหมาะกับการสร้างผลลัพธ์ที่มีชื่อฟิลด์ ส่วน multi-select list เหมาะกับการใช้ตามตำแหน่ง ทั้งสองแบบซ้อนใน projection ได้ แต่ต้องระวัง: ใน multi-select hash คีย์ที่ค่าของมันเป็น null จะยังคงอยู่ ขณะที่ projection จะทิ้งสมาชิก null ทั้งตัว
5. ฟังก์ชันในตัว
มาตรฐาน JMESPath กำหนดชุดฟังก์ชันไว้ ฟังก์ชันที่พบบ่อยได้แก่:
| ฟังก์ชัน | จุดประสงค์ | ตัวอย่าง |
|---|---|---|
length() |
หาความยาว | length(people) |
sort() |
เรียงค่า | sort(people[*].age) |
sort_by() |
เรียงตามฟิลด์ | sort_by(people, &age) |
max() / min() |
ค่าสูงสุด / ต่ำสุด | max(people[*].age) |
sum() |
รวมค่า | sum(people[*].age) |
keys() |
ดึงคีย์ของออบเจกต์ | keys(people[0]) |
contains() |
ตรวจว่ามีค่าอยู่หรือไม่ | contains(name, "Ali") |
to_string() |
แปลงเป็นสตริง | to_string(age) |
อาร์กิวเมนต์ตัวที่สองของ sort_by คือนิพจน์ & ซึ่งหมายถึง "ดึงฟิลด์นี้จากสมาชิกแต่ละตัว":
sort_by(people, &age)[*].name
ฟังก์ชันเหล่านี้ทำให้ JMESPath ทำการ aggregate อย่างง่ายได้ ขณะที่ JSONPath ถูกออกแบบมาเพื่อการเลือกเท่านั้น การรวมผลและการจัดกลุ่มมักต้องใช้ jq หรือเขียนโค้ดเอง
6. เปรียบเทียบกับ JSONPath แบบเคียงข้าง
| มิติ | JSONPath | JMESPath |
|---|---|---|
| เป้าหมายหลัก | เลือกโหนด | แปลงข้อมูล |
| สัญลักษณ์ราก | $ |
ไม่มี; เขียนชื่อฟิลด์โดยตรง |
| โหนดปัจจุบัน | @ |
@ (เฉพาะในอาร์กิวเมนต์ของฟังก์ชัน) |
| ตัวกรอง | [?(@.age > 18)] |
[?age > 18] |
| Wildcard | [*] |
[*] หรือ [] |
| สร้างออบเจกต์ใหม่ | ไม่รองรับ | {name: name} |
| Pipe | ไม่รองรับ | | |
| มาตรฐาน | RFC 9535 | ข้อกำหนด JMESPath |
แนวทางตัดสินใจหนึ่งประโยค:
- ใช้ JSONPath เมื่อต้องการเพียงค้นหาโหนดที่ตรงเงื่อนไข;
- ใช้ JMESPath เมื่อต้องปรับรูปผลลัพธ์ให้เป็นอย่างที่ frontend คาดหวัง;
- ไม่ต้องใช้ทั้งสองแบบเมื่ออ่านฟิลด์เดียวที่รู้อยู่แล้ว; เข้าถึงพร็อพเพอร์ตี้โดยตรงก็พอ
ถ้าผลลัพธ์การคิวรีต้องถูกเขียนกลับลงเอกสาร ทั้งสองภาษาไม่เหมาะ ให้ใช้ JSON Pointer และแพตช์แทน ดู JSON Pointer, Patch และ Merge Patch
7. JMESPath ใน AWS CLI
พารามิเตอร์ --query ของ AWS CLI คือ JMESPath ซึ่งเป็นการใช้งานที่แพร่หลายที่สุด:
aws ec2 describe-instances \
--query 'Reservations[].Instances[].{Id: InstanceId, State: State.Name, Type: InstanceType}' \
--output table
คำสั่งนี้ทำสามสิ่ง: ยุบลิสต์ซ้อนสองชั้นด้วย [], เก็บไว้เพียงสามฟิลด์ด้วย multi-select hash และแสดงผลเป็นตารางด้วย --output table ถ้าใช้ JSONPath คุณจะต้องเพิ่มระดับอีกและยังไม่ได้ผลลัพธ์แบบ "เปลี่ยนชื่อพร้อมตัดทอน"
รูปแบบอื่นที่พบบ่อย:
# เฉพาะ instance ที่กำลังทำงาน
aws ec2 describe-instances --query 'Reservations[].Instances[?State.Name==`running`].[InstanceId,PublicIpAddress]'
# ค่าของแท็ก Name
aws ec2 describe-instances --query 'Reservations[].Instances[].Tags[?Key==`Name`].Value[]'
โปรดสังเกตว่า backtick ใช้ล้อม literal ไม่ใช่สตริง ใน AWS CLI สตริงใช้ single quote และ JSON literal ใช้ backtick ความต่างนี้เป็นหนึ่งในข้อผิดพลาดที่ผู้เริ่มต้นพบมากที่สุด
8. ข้อผิดพลาดที่พบบ่อย
- สับสนระหว่าง
[*]กับ[]: แบบแรกคงการซ้อนไว้ แบบหลังยุบหนึ่งชั้น และโครงสร้างผลลัพธ์ที่ได้ต่างกันโดยสิ้นเชิง; - ลืม pipe:
people[*].name[0]เอาตัวอักษรตัวแรกของทุกชื่อ ไม่ใช่ชื่อตัวแรก; - เขียน
@ในตัวกรอง: ตัวกรองของ JMESPath ใช้ชื่อฟิลด์โดยตรง ดังนั้น[?@.age > 18]จึงผิด; - ถือว่า
nullเป็นสตริงว่าง: projection ทิ้งnullแต่ multi-select hash เก็บคีย์ไว้; - ใส่ double quote รอบ literal ใน AWS CLI: shell จะขยายมันก่อนและเขียนนิพจน์ใหม่
คำแนะนำในการดีบัก: ตรวจสอบนิพจน์กับอินพุตที่เล็กที่สุดก่อนนำไปใช้กับข้อมูลจริง และเพิ่ม --output json ใน AWS CLI เพื่อดูผลลัพธ์ดิบ สำหรับการแก้ไขและจัดรูปแบบ JSON ในชีวิตประจำวัน การใช้ร่วมกับ ตัวแก้ไข JSON และเครื่องมือบรรทัดคำสั่ง จะทำให้การหาพาธฟิลด์เร็วขึ้นมาก
คำถามที่พบบ่อย
ใช้ JMESPath และ JSONPath ร่วมกันได้ไหม
ได้ แต่โดยปกติไม่จำเป็น แนวทางที่พบบ่อยคือตัดทอนออบเจกต์เล็ก ๆ ด้วย JMESPath แล้วเข้าถึงพร็อพเพอร์ตี้แบบปกติในโค้ด ถ้าต้องระบุตำแหน่งตามพาธแล้วแก้ไขจริง ๆ ให้ JSONPath อ่านและ JSON Pointer เขียน ซึ่งเป็นการแบ่งงานที่ชัดเจนกว่า
ทำไม people[*].name ไม่ได้คืนค่าเป็นสตริงเดียว
เพราะ [*] เป็น projection และผลลัพธ์เป็นลิสต์เสมอ แม้จะมีคนเพียงคนเดียว ถ้าต้องการค่าเดียว ให้เพิ่ม pipe และดัชนี: people[*].name | [0]
ค่า null ใน projection เกิดอะไรขึ้น
List projection จะข้ามสมาชิกที่ประเมินแล้วได้ null โดยอัตโนมัติ ผลลัพธ์จึงอาจสั้นกว่าลิสต์เดิม ส่วน multi-select hash จะเก็บคีย์ไว้และรักษาค่า null ไว้ ซึ่งสำคัญเมื่อสร้างการตอบกลับของ API
JMESPath ทำ aggregation และการจัดกลุ่มได้ไหม
ทำ aggregation พื้นฐานได้ด้วย sum(), max() และ sort_by() การจัดกลุ่ม, join และ window function อยู่นอกมาตรฐาน ปล่อยการวิเคราะห์ซับซ้อนให้ jq, SQL หรือโค้ด
จะตัดสินใจระหว่าง JMESPath กับ JSONPath อย่างไร
ใช้ JSONPath เมื่อต้องเลือกโหนดตามพาธและเงื่อนไขแล้วได้ชุดโหนด; ใช้ JMESPath เมื่อต้องปรับรูปผลลัพธ์ให้เป็นอย่างที่ frontend คาดหวัง พร้อมเปลี่ยนชื่อและตัดทอนฟิลด์ เมื่ออ่านฟิลด์เดียวที่รู้อยู่แล้ว ไม่ต้องใช้ทั้งสองแบบ; เข้าถึงพร็อพเพอร์ตี้โดยตรงก็พอ
ควรใช้ pipe เพื่อเอาค่าแรกของ projection ไหม
ควร people[*].name เป็น projection ผลลัพธ์จึงเป็นลิสต์เสมอ; ถ้าไม่มี pipe, people[*].name[0] จะเอาตัวอักษรตัวแรกของทุกชื่อ เขียน people[*].name | [0] เพื่อให้ได้ค่าแรก