JSON Toolkit

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

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

ระดับกลางอ่านประมาณ 10 นาทีเผยแพร่ 2026-10-31อัปเดต 2026-10-31
ภาพปกสำหรับ JSONPath Filters เจาะลึก

สรุปสั้น: ระบุตำแหน่งที่รู้ความลึกก่อน แล้วค่อยเพิ่ม slice และตัวกรอง แทนที่จะคว้า $.. มาใช้; พาธเปล่าในตัวกรองตรวจเพียงการมีอยู่ ดังนั้นหากต้องการตรวจ boolean ให้เขียน @.stock == true

ใครก็เขียน $.store.book[0].title ได้ สิ่งที่ทำให้ JSONPath พังจริง ๆ คือการผสม $.. กับ slice และหลายเงื่อนไข แล้วได้ผลลัพธ์มากเกินไป น้อยเกินไป หรือได้ลำดับที่ไม่คาดคิด

1. เอกสารตัวอย่าง

{
  "store": {
    "book": [
      { "title": "JSON: The Definitive Guide", "price": 8.95, "stock": true, "tags": ["json", "spec"] },
      { "title": "A History of Data Formats", "price": 12.99, "stock": false, "tags": ["history"] },
      { "title": "How Parsers Work", "price": 8.99, "stock": true, "tags": ["parser", "json"] },
      { "title": "Streaming JSON in Practice", "price": 9.5, "stock": true, "tags": [] }
    ],
    "bicycle": { "color": "red", "price": 19.95 }
  },
  "updatedAt": "2026-10-31"
}

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

2. Recursive Descent $..: สะดวกแต่มีต้นทุน

.. หมายถึง "ค้นหาแบบเรียกซ้ำที่ความลึกใดก็ได้จากตำแหน่งปัจจุบัน" RFC 9535 กำหนดให้มันเป็น descendant segment และสามารถใช้ร่วมกับชื่อ wildcard ดัชนี slice และตัวกรองได้:

$..price                  // every member named price at any depth
$..book[?(@.price < 10)]  // any book at any depth, then filter
$..[?(@.stock)]           // any node at any depth where stock exists

สามสิ่งที่ต้องเข้าใจ:

  1. $..book ค้นหาโหนดทุกตัวที่ชื่อ book โดยไม่สนใจความลึก;
  2. การท่องเป็นแบบ depth-first และลำดับผลลัพธ์ขึ้นกับวิธีที่ implementation เยี่ยมชมลูก;
  3. .. ไม่สามารถใช้ดัชนีใด ๆ ได้ ต้นทุนจึงโตตามจำนวนโหนดทั้งหมด

ด้วยเหตุนี้ $..price กับ $.store.book[*].price อาจต่างกันเพียงราคาจักรยานในผลลัพธ์ แต่ต้นทุนต่างกันเป็นเท่าตัว เมื่อรู้ความลึก ควรเขียนพาธแบบชัดเจนเสมอ ถ้าต้องการเพียงเปรียบเทียบขอบเขตของภาษาพาธสองแบบ ดู JSONPath กับ JSON Pointer

3. Slice และ Wildcard ของ Array: กับดักสามข้อ

ไวยากรณ์ slice คือ [start:end:step] โดยรวมจุดเริ่มและไม่รวมจุดสิ้นสุด และจำนวนลบจะนับจากท้าย:

นิพจน์ ความหมาย ผลลัพธ์ (มีหนังสือสี่เล่ม)
[0:2] หนังสือสองเล่มแรก เล่มที่ 1 และ 2
[1:] จากเล่มที่สองถึงท้าย เล่มที่ 2, 3 และ 4
[:2] หนังสือสองเล่มแรก เล่มที่ 1 และ 2
[-2:] สองเล่มสุดท้าย เล่มที่ 3 และ 4
[::2] เว้นเล่มละหนึ่ง เล่มที่ 1 และ 3
[::-1] ลำดับย้อนกลับ เล่มที่ 4, 3, 2 และ 1

กับดักสามข้อ:

  • step เป็นศูนย์ไม่ถูกต้อง RFC 9535 กำหนดให้ step ไม่เป็นศูนย์ ขณะที่ implementation บางตัวคืนชุดว่างอย่างเงียบ ๆ;
  • step ติดลบไม่ได้รับการรองรับทุกที่ implementation ดั้งเดิมของ Goessner รองรับ แต่ implementation ขนาดเล็กบางตัวรับเฉพาะ step บวก;
  • การกรองหลัง slice ไม่ขยายขอบเขต [0:2][?(@.stock)] กรองเพียงสองเล่มที่ slice สร้างออกมา ไม่ได้ย้อนกลับไปทั้ง array

Wildcard [*] ใช้ร่วมกับ slice ได้ แต่ [*] ให้ "โหนดลูกทั้งหมด" ถ้าลูกเหล่านั้นเป็นออบเจกต์ การต่อ .title จะให้ทุก title ถ้าลูกตัวหนึ่งเป็น array เอง .title จะไม่ยุบอีกชั้นโดยปริยาย เมื่อต้องขยาย ให้เขียน [*] อีกครั้ง; อย่าหวังการยุบโดยปริยาย

4. ตัวกรองหลายเงื่อนไข: การเปรียบเทียบ ตรรกะ และการมีอยู่

ตัวกรอง [?(...)] บรรจุโครงสร้างได้สี่ชนิด: การเปรียบเทียบ ตัวดำเนินการทางตรรกะ การทดสอบการมีอยู่ และฟังก์ชัน คิวรีหลายเงื่อนไขที่พบบ่อยมีลักษณะดังนี้:

$..book[?(@.price < 10 && @.stock)]

มันหมายถึง "ราคาต่ำกว่า 10 และ stock เป็น truthy" ให้ใส่ใจรายละเอียดเชิงความหมายหลายประการ:

  • @.stock เป็นการทดสอบการมีอยู่ ไม่ใช่ "ค่าเป็น true" ถ้าฟิลด์มีอยู่แต่ค่าเป็น false เงื่อนไข ยังคงเป็นจริง การทดสอบค่า boolean ต้องเขียน @.stock == true;
  • && ผูกแน่นกว่า || ให้ใส่วงเล็บเมื่อผสมกัน แทนที่จะพึ่งความจำ;
  • ทั้งสองฝั่งของการเปรียบเทียบต้องมีชนิดเดียวกัน ตัวเลขกับสตริงไม่ถูกแปลงโดยปริยาย; @.price < "10" เป็น false หรือ error ใน implementation ที่เข้มงวด;
  • ฟิลด์ที่หายไปทำให้การเปรียบเทียบเป็น false เสมอ @.missing < 10 ไม่ก่อ error; เพียงแต่ไม่ตรงกับอะไรเลย

นี่คือจุดที่พลาดง่ายที่สุดเมื่อเขียนตัวกรองราวกับเป็น JavaScript: if (obj.stock) ทดสอบ truthiness ใน JavaScript ขณะที่ JSONPath ทดสอบการมีอยู่ สำหรับไวยากรณ์และฟังก์ชันมาตรฐานทั้งหมด ให้กลับไปอ่าน ภาษา JSONPath ในทางปฏิบัติ

5. Script Expression: ทรงพลังขึ้นแต่พกพาได้น้อยลง

implementation ดั้งเดิมของ Goessner และ jsonpath-plus อนุญาตการเปรียบเทียบข้ามโหนดอย่าง [?(@.price < $.max)] และแม้แต่ script ตามอำเภอใจ RFC 9535 อนุญาตอย่างชัดเจนเฉพาะการเข้าถึงโหนดปัจจุบัน @ และห้าม $ ภายในตัวกรอง

// ใช้ได้ใน implementation เก่า; RFC 9535 ไม่อนุญาต
$..book[?(@.price < $.expensive)]

// แนวทางมาตรฐาน: ส่ง threshold จากผู้เรียก
query(document, '$..book[?(@.price < threshold)]', { threshold: 10 })

Script expression สร้างปัญหาสามข้อ:

  1. ความสามารถพกพาลดลงเหลือศูนย์ การเปลี่ยนไลบรารีหมายถึงเขียนนิพจน์ทุกตัวใหม่;
  2. ขอบเขตความปลอดภัยหายไป เมื่อนิพจน์มาจากผู้ใช้ การประเมิน script เท่ากับมอบการรันโค้ดให้;
  3. การปรับให้เหมาะสมเชิงสถิตเป็นไปไม่ได้ implementation ทำได้เพียงตีความทีละโหนด ซึ่งช้าและแคชยาก

ถ้าต้องการเปรียบเทียบข้ามโหนดจริง ๆ ให้ส่ง threshold เป็นพารามิเตอร์ของคิวรี แล้วกรองซ้ำในโค้ดแอปพลิเคชัน วิธีนี้มักเสถียรกว่าการพึ่งไวยากรณ์ส่วนขยาย

6. รูปร่างผลลัพธ์: คุณคิดว่าจะได้ค่า แต่กลับได้โหนด

นิพจน์เดียวกันอาจคืนอย่างน้อยสามรูปร่างในแต่ละไลบรารี:

["JSON: The Definitive Guide", "How Parsers Work", "Streaming JSON in Practice"] // อาร์เรย์ของค่า
[{ "path": "$['store']['book'][0]['title']", "value": "..." }]                 // พาธ + ค่า
[{ "pointer": "/store/book/0/title", "value": "..." }]                          // JSON Pointer + ค่า

สิ่งนี้กระทบสามเรื่องโดยตรง: วิธีที่ frontend อ่านผลลัพธ์ วิธีที่ผลลัพธ์ถูก deduplicate และวิธีที่ผลคิวรีกลายเป็น operation การเขียน nodelist ที่ RFC 9535 กำหนดคือชุดของ node reference แต่ implementation ของ JavaScript มักคืนเฉพาะค่า เพราะ reference ไม่สามารถข้ามขอบเขตโปรเซสได้

คำแนะนำในทางปฏิบัติ:

  • อ่านค่าเดี่ยวผ่าน [0] หรือ value() เสมอ; อย่าสมมติว่าผลลัพธ์เป็น scalar;
  • เมื่อต้องเขียนกลับ ให้เลือก implementation ที่คืน JSON Pointer หรือสร้าง pointer เอง;
  • ผลลัพธ์ว่างกับผลลัพธ์ [null] ต่างกัน; ตรวจ length แทนการตรวจ truthiness

7. ความแตกต่างระหว่างไลบรารี: รันชุดทดสอบความสอดคล้องก่อนย้าย

ความสามารถ Goessner ดั้งเดิม jsonpath-plus RFC 9535 เข้มงวด
วงเล็บในตัวกรอง ?() จำเป็น เข้ากันได้ ไม่บังคับ
การอ้างอิงข้ามราก $.x รองรับ รองรับ ห้าม
Script expression รองรับ รองรับ ห้าม
ดัชนีและ step ติดลบ รองรับ รองรับ รองรับ
คืนพาธ ตั้งค่าได้ ตั้งค่าได้ ไม่ได้กำหนด
ฟังก์ชันอย่าง length() ไม่มี ส่วนขยาย ฟังก์ชันมาตรฐาน

ก่อนย้าย ให้ยืนยันอย่างน้อยสี่เรื่อง: วงเล็บจำเป็นหรือไม่, $ ปรากฏในตัวกรองได้หรือไม่, step ติดลบทำงานอย่างไร และโครงสร้างผลลัพธ์ ชุดทดสอบความสอดคล้องอย่างเป็นทางการครอบคลุมความต่างทางไวยากรณ์ส่วนใหญ่ และป้องกันความล้มเหลวเงียบ ๆ อย่าง "หลังเปลี่ยนไลบรารีแล้วสามแถวหายไป"

8. ประสิทธิภาพและรายการตรวจสอบการใช้งานผิด

ประสิทธิภาพสรุปได้หนึ่งประโยค: JSONPath คือการท่องทั้งเอกสาร และ .. คือการท่องทั้งเอกสารที่แพงที่สุด

  • สำหรับเอกสารที่มีโหนดเกิน 100,000 ให้จำกัดขอบเขตด้วยพาธแบบชัดเจนก่อน แล้วค่อยกรองชุดย่อย;
  • compile นิพจน์ครั้งเดียวแล้วใช้ซ้ำ; อย่า parse สตริงใหม่ในลูป;
  • เปลี่ยนไปใช้ jq หรือโค้ดสำหรับ aggregation (รวม หาจัดกลุ่ม เรียงลำดับ); JSONPath เลือกเท่านั้น;
  • วางเงื่อนไขให้เร็วที่สุด: $.store.book[?(@.price < 10)] สแกนโหนดที่ไม่เกี่ยวข้องน้อยกว่า $..[?(@.price < 10)] มาก

รายการตรวจสอบการใช้งานผิด:

  • [ ] ถือว่าการทดสอบการมีอยู่เป็นการทดสอบค่า boolean;
  • [ ] ใช้ $.. แทนความลึกที่รู้อยู่แล้ว;
  • [ ] พึ่ง script expression เพื่อเปรียบเทียบข้ามโหนด;
  • [ ] สมมติว่าผลลัพธ์เป็นอาร์เรย์ของค่าเสมอ;
  • [ ] ปล่อยให้นิพจน์ถูกควบคุมโดยผู้ใช้โดยไม่มีขีดจำกัดความยาวและความซ้อน

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

ทำไม @.stock ยังตรงเงื่อนไขเมื่อฟิลด์เป็น false

เพราะพาธเปล่าในตัวกรองหมายถึง "ฟิลด์มีอยู่" ไม่ใช่ "ค่าเป็น true" การทดสอบ boolean ต้องเปรียบเทียบอย่างชัดเจนกับ @.stock == true นี่คือความต่างเชิงความหมายที่ใหญ่ที่สุดระหว่าง JSONPath กับ truthiness ของ JavaScript

ลำดับของ recursive descent เสถียรหรือไม่

ไม่ RFC 9535 กำหนดความหมายแบบชุดโหนด และลำดับการท่องขึ้นกับวิธีที่ implementation เยี่ยมชมลูก เมื่อต้องการลำดับคงที่ ให้ระบุ array แล้วอ่านตามดัชนี หรือเรียงผลลัพธ์ในโค้ดแอปพลิเคชัน

ใช้ slice และตัวกรองร่วมกันได้ไหม

ได้ แต่ต้องจำขอบเขต $.store.book[0:2][?(@.stock)] กรองเพียงสองเล่มที่ slice สร้างออกมา ถ้าต้องกรองก่อนแล้ว slice ทีหลัง ให้เขียน $.store.book[?(@.stock)][0:2]; ลำดับต่างกันให้ผลลัพธ์ต่างกัน

ทำไม jsonpath-plus รองรับ $.expensive ขณะที่มาตรฐานห้าม

เพราะตัวกรองถูกกำหนดโดยอิงเฉพาะโหนดปัจจุบัน @ การอ้างอิงข้ามรากทำให้นิพจน์ขึ้นกับบริบทของทั้งเอกสาร ซึ่งวิเคราะห์เชิงสถิตและพอร์ตข้ามภาษาได้ยาก ให้ส่ง threshold เป็นพารามิเตอร์แทนการฝังในนิพจน์

จะป้องกัน recursive descent ไม่ให้สแกนโหนดที่ไม่เกี่ยวข้องได้อย่างไร

เขียนพาธแบบชัดเจนทุกครั้งที่รู้ความลึก จำกัดขอบเขตก่อน แล้วกรองเฉพาะชุดย่อยแทนการใช้ $.. แทนระดับที่รู้อยู่แล้ว วางเงื่อนไขให้เร็วที่สุด: $.store.book[?(@.price < 10)] สแกนโหนดที่ไม่เกี่ยวข้องน้อยกว่า $..[?(@.price < 10)] มาก สำหรับเอกสารที่มีโหนดเกิน 100,000 ให้ทำเช่นนี้ก่อน

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

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

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

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

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

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

JSON Pointer, Patch และ Merge Patch

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

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

JSONPath หรือ JSON Pointer

JSON Pointer คือที่อยู่ที่ไม่ซ้ำซึ่งเก็บเป็นคีย์ได้; JSONPath คือภาษา query การเลือกใช้ผิดจะทำให้การอัปเดต config พังหรือต้องสร้าง JSONPath ที่ไม่สมบูรณ์ขึ้นมาเอง

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