JSON Toolkit

ภาษา JSONPath ในทางปฏิบัติ

สรุปสำคัญ: ในปี 2024 JSONPath มีมาตรฐานอย่างเป็นทางการในที่สุด นั่นคือ RFC 9535 บทความนี้อธิบายไวยากรณ์ ความหมายของตัวกรอง ฟังก์ชัน และความแตกต่างเชิงประวัติศาสตร์ระหว่าง implementation

ระดับกลางอ่านประมาณ 12 นาทีเผยแพร่ 2026-10-30อัปเดต 2026-10-30
ภาพปกสำหรับภาษา JSONPath ในทางปฏิบัติ

สรุปสั้น: ระบุตำแหน่งด้วยพาธแบบชัดเจนก่อน แล้วกรองบนพาธนั้น และเขียนนิพจน์ตาม RFC 9535; ใช้การเข้าถึงพร็อพเพอร์ตี้หรือ JSON Pointer สำหรับฟิลด์เดียวที่ตายตัว และใช้ jq หรือโค้ดสำหรับ aggregation

คุณคงเคยเห็นนิพจน์อย่าง $.store.book[?(@.price < 10)].title แต่เพิ่งกลายเป็นมาตรฐานสากลในปี 2024

1. จากมาตรฐานโดยพฤตินัยสู่มาตรฐานอย่างเป็นทางการ

JSONPath ถูกเสนอโดย Stefan Gössner ในปี 2007 และเป็นเวลานาน "ข้อกำหนด" เดียวคือบล็อกโพสต์เดียว โดยแต่ละ implementation ในแต่ละภาษาต่างทำตามใจตัวเอง:

  • ตัวกรองต้องมีวงเล็บหรือไม่ (?(@.a) กับ ?@.a);
  • รองรับดัชนี array [-1] หรือไม่;
  • ผลลัพธ์คืน "ค่า" หรือ "ค่าพร้อมพาธ";
  • อนุญาต script expression (?(@.price < $.max)) หรือไม่

RFC 9535 ที่เผยแพร่ในเดือนกุมภาพันธ์ 2024 ได้หลอมความแตกต่างเหล่านี้ให้เป็นไวยากรณ์และความหมายชุดเดียว พร้อมชุดทดสอบความสอดคล้องอย่างเป็นทางการ โปรเจกต์ใหม่ควรปฏิบัติตาม RFC 9535; เมื่อย้ายโปรเจกต์เก่า ให้ยืนยันเวอร์ชันของ implementation ก่อน

2. ตารางสรุปไวยากรณ์

ไวยากรณ์ ความหมาย ตัวอย่าง
$ โหนดราก $.store
@ โหนดปัจจุบันภายในตัวกรอง $[?@.price < 10]
.name โหนดลูก (ชื่อต้องตรงกับ [A-Za-z_][A-Za-z0-9_]*) $.store.book
['name'] โหนดลูก (ชื่อใดก็ได้ รวมถึงอักขระที่ไม่ใช่ ASCII และจุด) $['store']['书']
[0] ดัชนี array; จำนวนลบนับจากท้าย $.book[0], $.book[-1]
[0,2] หลายดัชนี / หลายชื่อ $.book[0,2]
[1:3] Slice โดยรวมจุดเริ่มและไม่รวมจุดสิ้นสุด $.book[1:3]
[::2] Slice พร้อม step $.book[::2]
[*] Wildcard สำหรับโหนดลูกทั้งหมด $.book[*].title
..name Recursive descent ค้นหาที่ความลึกใดก็ได้ $..price
..[*] ดึงโหนดทั้งหมดแบบเรียกซ้ำ $..*
[?<expr>] Filter selector $.book[?@.price < 10]

ความหมายหนึ่งที่ต้องจำ: JSONPath คืนค่ากลับเป็นชุดโหนด (nodelist) ไม่ใช่ค่าเดี่ยว แม้จะตรงกับโหนดเพียงหนึ่งเดียว ผลลัพธ์ก็ยังเป็นลิสต์ที่มีความยาวหนึ่ง

3. ทดลองไวยากรณ์ด้วยตัวอย่างเดียว

{
  "store": {
    "book": [
      { "title": "JSON 权威指南", "price": 8.95, "tags": ["json", "spec"] },
      { "title": "数据交换格式史", "price": 12.99, "tags": ["history"] },
      { "title": "解析器实现原理", "price": 8.99, "tags": ["parser", "json"] }
    ],
    "bicycle": { "color": "red", "price": 19.95 }
  },
  "expensive": 10
}
นิพจน์ ผลลัพธ์
$.store.bicycle.color ["red"]
$..price ราคา 4 ค่า (รวมราคาจักรยาน)
$.store.book[*].title title 3 รายการ
$.store.book[?@.price < 10].title title ของเล่มที่ 1 และ 3
$.store.book[?@.tags] หนังสือทุกเล่มที่มี tags (ทดสอบการมีอยู่)
$.store.book[-1].title title ของเล่มสุดท้าย
$..book[0,1] สองเล่มแรก
$.store.book[?@.price < $.expensive] ขึ้นกับ implementation ตามไวยากรณ์; RFC 9535 ไม่อนุญาต การอ้างอิงข้ามราก

แถวสุดท้ายคือกับดักทางประวัติศาสตร์: implementation จำนวนมากอนุญาต $.expensive ในตัวกรองเพื่อเปรียบเทียบข้ามโหนด ซึ่ง RFC 9535 ห้ามอย่างชัดเจน — ตัวกรองเข้าถึงได้เฉพาะโหนดปัจจุบัน @ เมื่อต้องเปรียบเทียบกับ threshold ระดับโลก ให้ส่ง threshold เป็นอาร์กิวเมนต์ของฟังก์ชันคิวรี

4. ตัวกรองแสดงอะไรได้บ้าง

นิพจน์ตัวกรองของ RFC 9535 รองรับโครงสร้างสี่ชนิด:

ตรรกะ:       &&  ||  !          e.g. ?@.price < 10 && @.tags
การเปรียบเทียบ:  ==  !=  <  <=  >  >=
การมีอยู่:   พาธเปล่า        e.g. ?@.tags (tests existence)
ฟังก์ชัน:   length() count() match() search() value()

ตัวอย่างในทางปฏิบัติหลายข้อ:

// ราคาระหว่าง 8 ถึง 10
$.store.book[?@.price >= 8 && @.price <= 10]

// title มี "JSON" (regex match; อาร์กิวเมนต์ที่สองเป็น I-Regexp)
$.store.book[?match(@.title, ".*JSON.*")]

// มีแท็กมากกว่าหนึ่ง
$.store.book[?length(@.tags) > 1]

// ดึงค่าแรกที่ตรงแทนลิสต์ (value() เปลี่ยนชุดโหนดเดียวให้เป็นค่า)
$.store.book[?@.price < 9].title.value()

match() เป็นการจับคู่ทั้งสตริง และ search() เป็นการค้นหาสตริงย่อย; ภาษาปกติของ regex ที่ใช้คือ I-Regexp (ชุดย่อยของ RFC 9485) ซึ่งไม่รองรับทั้ง backreference และ lookaround — นั่นเป็นทั้งข้อจำกัดและเกราะป้องกัน regex DoS ที่ดี

5. ความแตกต่างระหว่าง implementation: ตรวจสอบก่อนย้าย

Implementation ภาษา การรองรับ RFC 9535 หมายเหตุ
jsonpath-plus JavaScript เข้ากันได้เป็นส่วนใหญ่ รองรับฟังก์ชันส่วนขยายและ script expression; สามารถมากกว่าแต่ไม่เข้มงวดตามมาตรฐาน
jsonpath (dchester) JavaScript บางส่วน ไลบรารีที่อยู่มานาน มีไวยากรณ์ ?() และโครงสร้างผลลัพธ์ต่างออกไป
ojg / gjson Go ojg รองรับดี gjson ใช้ไวยากรณ์พาธอีกแบบ ไม่ใช่ JSONPath
jsonpath-ng Python บางส่วน รองรับส่วนขยายและค่อนข้างผ่อนปรน
Jayway JsonPath Java บางส่วน ระบบนิเวศกว้าง; พฤติกรรมเชิงประวัติศาสตร์ต่างจาก RFC

การตรวจสอบสามข้อก่อนย้าย:

  1. วงเล็บในตัวกรองจำเป็นหรือไม่;
  2. รองรับดัชนีลบอย่าง -1 หรือไม่;
  3. ผลลัพธ์เป็น "อาร์เรย์ของค่า" หรือ "อาร์เรย์ของออบเจกต์ {path, value}"

ข้อที่สามพลาดง่ายที่สุด: frontend อ่าน result[0] และหลังเปลี่ยนไลบรารีกลับได้ออบเจกต์ ทำให้หน้าว่างเปล่า

6. ประสิทธิภาพและความปลอดภัย

ประสิทธิภาพ

  • JSONPath เป็น การท่องครั้งเดียว ที่มีความซับซ้อน O(จำนวนโหนด) และไม่สามารถใช้ดัชนีได้;
  • Recursive descent ด้วย .. มีต้นทุนสูงที่สุด; ถ้าระบุตำแหน่งด้วย $.store.book ก่อนแล้วกรองได้ ก็อย่าเขียน $..book;
  • compile นิพจน์ครั้งเดียวแล้วใช้ซ้ำ (ไลบรารีส่วนใหญ่มี compile หรือแคช) แทนการ parse ใหม่ทุกครั้งที่คิวรี;
  • สำหรับเอกสารที่มีโหนดเกิน 100,000 ให้พิจารณาใช้ JSON Pointer ดึงข้อมูลแบบตรงจุดก่อน หรือประมวลผลล่วงหน้าที่เซิร์ฟเวอร์ด้วย jq

ความปลอดภัย

  • นิพจน์จากอินพุตของผู้ใช้ต้องมีขีดจำกัดความยาวและความลึกของการซ้อน;
  • อย่าให้ JSONPath จัดการการอนุญาต — มันอ่านได้เท่านั้น และไม่สามารถแสดง "ฟิลด์ใดมองไม่เห็น";
  • เมื่อ regex ที่ match() ใช้มาจากผู้ใช้ ให้จำกัดความซับซ้อนของมัน; แม้จะปลอดภัยกว่า PCRE แต่ยังแนะนำให้จำกัดความยาว

7. เมื่อไม่ควรใช้ JSONPath

  • ต้องการฟิลด์เดียวที่ตายตัว → ใช้ data.store.bicycle.color โดยตรง;
  • ต้องเขียนกลับหลังแก้ไข → ใช้ JSON Pointer หรือแพตช์; JSONPath ไม่เขียนกลับ;
  • ต้องทำ aggregation (รวม หาจัดกลุ่ม) → ใช้ jq, SQL หรือโค้ด; JSONPath เลือกเท่านั้น;
  • เอกสารใหญ่และต้องการเพียงไม่กี่ฟิลด์ → ใช้ streaming parser แทนการอ่านต้นไม้ทั้งก้อนเข้าหน่วยความจำก่อน

8. รายการตรวจสอบ

  • [ ] นิพจน์เป็นไปตาม RFC 9535 และไม่พึ่งการอ้างอิงข้ามราก;
  • [ ] ชัดเจนว่าผลลัพธ์เป็นชุดโหนด; ใช้ [0] หรือ value() เพื่อให้ได้ค่าเดี่ยว;
  • [ ] นิพจน์ถูก compile ล่วงหน้าและใช้ซ้ำ;
  • [ ] นิพจน์ที่ผู้ใช้ควบคุมมีขีดจำกัดความยาวและความซับซ้อน;
  • [ ] รันกรณีทดสอบความสอดคล้องอย่างเป็นทางการเมื่อเปลี่ยน implementation

คำถามที่พบบ่อย

จะเลือก JSON Pointer กับ JSONPath อย่างไร

ใช้การเข้าถึงพร็อพเพอร์ตี้ปกติหรือ JSON Pointer เมื่อต้องการฟิลด์เดียวที่ตายตัว; เลือก JSONPath เมื่อต้องการตัวกรอง wildcard หรือ recursive descent JSONPath เลือกเท่านั้นและไม่เขียนกลับ การเขียนกลับจึงเป็นของ JSON Pointer หรือแพตช์

ทำไมตัวกรองอ้างอิง $.expensive ไม่ได้

RFC 9535 ห้ามการอ้างอิงข้ามรากอย่างชัดเจน; ตัวกรองเข้าถึงได้เฉพาะโหนดปัจจุบัน @ ให้ส่ง threshold เป็นอาร์กิวเมนต์ของฟังก์ชันคิวรีแทน implementation เก่ารับไวยากรณ์นี้ได้ แต่จะพังเมื่อเปลี่ยนหรืออัปเกรดไลบรารี

ควรพึ่ง result[0] ในการอ่านค่าหรือไม่

ใช้ได้หลังยืนยันว่า implementation ปัจจุบันคืนอาร์เรย์ของค่า ไลบรารีบางตัวคืนออบเจกต์ {path, value} ทำให้ result[0] กลายเป็นออบเจกต์และหน้าว่างเปล่า ให้อ่านค่าเดี่ยวผ่าน [0] หรือ value() และรันชุดทดสอบความสอดคล้องอย่างเป็นทางการก่อนเปลี่ยน implementation

แหล่งอ้างอิง

เครื่องมือที่เกี่ยวข้อง

บทความที่เกี่ยวข้อง

JSON Pointer, Patch และ Merge Patch

ข้อกำหนดสั้นสามฉบับประกอบกันเป็นโปรโตคอลการเปลี่ยนแปลง: Pointer ระบุตำแหน่ง, Patch แก้ไขอย่างแม่นยำ, Merge Patch รวมบางส่วน การเลือกผิดจะสร้างปัญหาให้ array และ concurrency

ระดับกลางอ่านประมาณ 12 นาทีอัปเดต 2026-10-30

เปรียบเทียบไลบรารีสำหรับ Parse และประมวลผล JSON

API พื้นเมืองเร็วที่สุด parser ที่ยอมรับข้อผิดพลาดให้ offset เลือกจากสามสิ่ง: การระบุตำแหน่ง error, ข้อความต้นฉบับถูกเก็บไว้หรือไม่ และขอบเขตชัดเจนหรือไม่

ระดับกลางอ่านประมาณ 11 นาทีอัปเดต 2026-10-30

JSONPath Filters เจาะลึก: Recursion, Slice และการจับคู่หลายเงื่อนไข

Recursive descent, slice และตัวกรองหลายเงื่อนไขคือจุดที่ JSONPath ทั้งทรงพลังและถูกใช้ผิด ครอบคลุมความหมายของตัวกรอง รูปร่างผลลัพธ์ และความแตกต่างระหว่างไลบรารี

ระดับกลางอ่านประมาณ 10 นาทีอัปเดต 2026-10-31