JSON Toolkit

อ่านขอบเขตไวยากรณ์ JSON จาก RFC 8259

สรุปสำคัญ: แปลกฎการผลิตทางไวยากรณ์ของ RFC 8259 เป็นภาษาธรรมดา: อักขระใดปรากฏได้ตรง ๆ อักขระใดต้อง escape และจุดใดที่มาตรฐานจงใจเปิดช่องไว้

ผู้เริ่มต้นอ่านประมาณ 11 นาทีเผยแพร่ 2026-10-30อัปเดต 2026-10-30
ภาพหน้าปกการอ่านขอบเขตไวยากรณ์ JSON จาก RFC 8259

สรุปสั้น: เมื่อเกิด JSON syntax error ให้หาอักขระผิดกฎตัวแรกแล้วจำแนกว่ามันคือเครื่องหมายคำพูดเดี่ยว เครื่องหมายจุลภาคท้ายรายการ คอมเมนต์ หรืออักขระควบคุม อย่าอ่านเอกสารใหม่ทั้งหมด; ฝั่งการเข้ารหัสให้ยืนยันว่าเป็น UTF-8 ที่ไม่มี BOM และส่งจำนวนเต็มที่เกิน 2^53 - 1 เป็นสตริง

เนื้อหามาตรฐานมีเพียงสิบกว่าหน้า แต่เบื้องหลังทุกคำว่า "MUST", "SHOULD" และ "MAY" ซ่อนเหตุการณ์ที่เคยทำให้ทั้งทีมนอนไม่หลับไว้หนึ่งครั้ง

1. เอกสารสามฉบับสัมพันธ์กันอย่างไร

การเรียนรู้ JSON มักสับสนเพราะมีเลขสามตัว: RFC 4627, RFC 7159, RFC 8259 บวกกับ ECMA-404 ความสัมพันธ์จริง ๆ ชัดเจนทีเดียว:

เอกสาร วันที่ สถานะ ความต่างสำคัญ
RFC 4627 2006 ถูกแทนที่แล้ว ระดับบนสุดต้องเป็น object หรือ array
RFC 7159 2014 ถูกแทนที่แล้ว ผ่อนปรนระดับบนสุดให้เป็นค่า JSON ใดก็ได้
RFC 8259 2017.12 มาตรฐานปัจจุบัน ระบุ UTF-8 ชัดเจน เพิ่มคำแนะนำด้านการทำงานร่วมกัน
ECMA-404 2013 ฉบับพิมพ์ครั้งที่สอง 2017.12 ข้อกำหนดไวยากรณ์ กำหนดเพียงไวยากรณ์ ไม่ครอบคลุมการเข้ารหัสหรือการส่งข้อมูล

ควรอ่านฉบับไหน? อ่าน RFC 8259 ฉบับเดียวก็พอ ECMA-404 ส่วนไวยากรณ์เทียบเท่ากัน; ต่างกันที่ RFC 8259 ยังระบุ "ส่งข้อมูลเป็น UTF-8" และ "ประเด็นการทำงานร่วมกันที่ผู้พัฒนาควรระวัง" ด้วย

2. ค่าหกประเภท

กฎการผลิตทางไวยากรณ์ของมาตรฐานย่อเป็นแผนภาพเดียวได้:

JSON-text  = ws value ws
value      = object / array / number / string / "true" / "false" / "null"
object     = "{" [ member *( "," member ) ] "}"
member     = string ws ":" ws value
array      = "[" [ value *( "," value ) ] "]"

มีจุดสำคัญเพียงหกข้อ:

  1. Object ใช้ปีกกา สมาชิกคั่นด้วยเครื่องหมายจุลภาค และเครื่องหมายจุลภาคหลังสมาชิกตัวสุดท้าย ไม่อนุญาต;
  2. Array ใช้วงเล็บเหลี่ยม องค์ประกอบคั่นด้วยเครื่องหมายจุลภาค และเครื่องหมายจุลภาคท้ายรายการก็ไม่อนุญาตเช่นกัน;
  3. ตัวเลข ห้ามมี + นำหน้า ห้ามมีศูนย์นำหน้า (01 ผิด) ห้ามมีจุดทศนิยมลอย (1. ผิด) และห้ามมี NaN หรือ Infinity;
  4. สตริง ต้องใช้เครื่องหมายคำพูดคู่ เครื่องหมายคำพูดเดี่ยวผิด;
  5. บูลีนและ null เป็นค่าคงที่ตัวพิมพ์เล็ก; True และ NULL ผิดทั้งคู่;
  6. ระหว่างองค์ประกอบเชิงโครงสร้าง ปรากฏได้เฉพาะอักขระช่องว่าง 0x20 (ช่องว่าง), 0x09 (แท็บ), 0x0A (line feed) และ 0x0D (carriage return).

ตัวอย่างด้านล่างเป็น JSON ที่ถูกต้อง ถึงแม้หลายคนคิดว่าผิดตอนเห็นครั้งแรก — เพราะมันเป็นสตริงระดับบนสุดเปล่า ๆ:

"just a string"

ตั้งแต่ RFC 7159 ระดับบนสุดเป็นค่าใดก็ได้จริง ใน API อย่างไรก็ตามยังแนะนำให้ใช้เฉพาะ object หรือ array; เหตุผลอยู่ในหัวข้อ 6

3. อะไรปรากฏได้ตรง ๆ ภายในสตริง

นี่คือส่วนที่ถูก "เครื่องผมรันได้ปกติดี" หลอกได้ง่ายที่สุด กฎของมาตรฐานสำหรับสตริงคือ:

  • อักขระที่ปรากฏได้ตรง ๆ: %x20-21, %x23-5B, %x5D-10FFFF;
  • พูดอีกอย่าง เครื่องหมายคำพูดคู่ " (0x22) และแบ็กสแลช \ (0x5C) ต้องถูก escape;
  • อักขระควบคุมทั้งหมดที่ต่ำกว่า 0x20 ต้องถูก escape รวมถึง line feed แท็บ และ carriage return;
  • ลำดับ escape ที่ใช้ได้มีเพียง \", \\, \/, \b, \f, \n, \r, \t และ \uXXXX.
{
  "ok": "Chinese text, emoji 🎉 and full-width symbols can be written directly",
  "escaped": "A line break goes here\nand a tab\tfinishes it",
  "control": "\u0007 is the bell character and must be escaped",
  "quote": "He said: \"don't use single quotes\""
}

\/ เป็น escape แบบเก่า (ใช้ป้องกัน </script> ปิดก่อนเวลา) ยุคปัจจุบันแทบไม่ต้องใช้ แต่ตัวแยกวิเคราะห์ต้องรองรับ

4. การเข้ารหัส: UTF-8 ไม่มี BOM

หมวด 8.1 ของ RFC 8259 กล่าวสามอย่าง:

  1. ข้อความ JSON ที่ใช้แลกเปลี่ยนข้อมูล ต้อง เข้ารหัสเป็น UTF-8;
  2. การนำไปใช้งาน ต้องไม่ เพิ่ม BOM (EF BB BF) ที่ต้นข้อความ;
  3. ตัวแยกวิเคราะห์ อาจ เพิกเฉยต่อ BOM เพื่อความทนทาน

ความล้มเหลวสองแบบที่พบจริง:

  • Notepad ของ Windows บันทึกเป็น "UTF-8 with BOM" ไฟล์ถูก JSON.parse ปฏิเสธด้วย Unexpected token;
  • ไฟล์ JSON ภาษาจีนที่บันทึกเป็น GBK กลายเป็นตัวขยะบนไปป์ไลน์ที่ถอดรหัสเฉพาะ UTF-8 แต่ไวยากรณ์ยังบังเอิญผ่าน error จึงถูกเลื่อนไปที่ชั้นธุรกิจ

การจัดการตรงไปตรงมา: ตัด BOM ตอนอ่านไฟล์ แล้วถอดรหัสเป็น UTF-8 อย่างชัดเจน; ตอนเขียนให้แน่ใจว่าไม่มีการเพิ่ม BOM

/**
 * Removes the UTF-8 BOM at the start of a string.
 * @param text The original text
 * @returns The text without a BOM
 */
function stripBom(text: string): string {
  return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
}

หมายเหตุเรื่อง media type ระหว่างนี้ด้วย: application/json ไม่รับพารามิเตอร์ charset แล้ว การเขียนเข้าไปไม่ทำให้เกิด error แต่เป็นแนวปฏิบัติที่ล้าสมัย

5. สามจุดที่มาตรฐานจงใจเปิดช่องไว้

จุดที่มาตรฐานไม่ยอมกำหนดให้แน่ชัดคือที่มาของความต่างระหว่างการนำไปใช้งานนั่นเอง

1. ช่วงค่าและความแม่นยำของตัวเลข

RFC 8259 บอกเพียงว่า "ข้อกำหนดนี้ไม่จำกัดช่วงค่าหรือความแม่นยำของตัวเลข" พร้อมหมายเหตุว่าการนำไปใช้งานส่วนใหญ่ใช้ IEEE 754 ความแม่นยำสองเท่า นี่หมายความว่า 9223372036854775807 ค่าเดียวกันกลายเป็น 9223372036854776000 ใน JavaScript แต่ยังแม่นยำใน long ของ Java

ข้อสรุป: จำนวนเต็มที่เกิน 2^53 - 1 และจำนวนเงินที่ต้องใช้ทศนิยมหลายตำแหน่ง ควรส่งเป็นสตริงเสมอ พร้อมเขียน pattern ไว้ใน schema อย่างชัดเจน

2. คีย์ซ้ำ

ถ้อยคำในมาตรฐานคือ "ชื่อภายใน object ควร (SHOULD) ไม่ซ้ำกัน"; ไม่ได้บอกว่าถ้าซ้ำแล้วต้องทำอย่างไร

  • JSON.parse เก็บตัวสุดท้าย;
  • ตัวแยกวิเคราะห์บางตัวเก็บตัวแรก;
  • ไลบรารีบางตัวแค่ throw error

ข้อสรุป: อย่าเขียนคีย์ซ้ำ และอย่าพึ่งพฤติกรรมใด ๆ เป็นพิเศษในตัวตรวจสอบของคุณ การเพิ่มขั้นตอน "ตรวจจับคีย์ซ้ำ" ตอนบิลด์ถูกกว่าการสืบสวนทีหลังมาก

3. ข้อจำกัดความลึกและความยาว

มาตรฐานไม่ได้กำหนดขีดจำกัดความลึกของการซ้อน แต่การนำไปใช้งานทุกตัวมีขีดจำกัด V8 throw RangeError: Maximum call stack size exceeded ที่การซ้อนระดับหลายหมื่นชั้น เซิร์ฟเวอร์ต้องกำหนดขีดจำกัดของตนเอง นี่เป็นข้อกำหนดด้านความปลอดภัย ไม่ใช่ข้อกำหนดของมาตรฐาน

6. ทดสอบความเข้าใจด้วยตัวอย่างที่ผิด

ทุกบรรทัดด้านล่างเป็น JSON ที่ไม่ถูกต้อง ป้อนให้ตัวแยกวิเคราะห์แล้วดูว่าข้อความ error มีประโยชน์หรือไม่:

{ 'name': 'json' }        // single quotes
{ name: "json" }          // key name not quoted
{"a": 1,}                 // trailing comma
[1, 2, 3,]                // trailing comma in an array
{"a": 01}                 // leading zero
{"a": +1}                 // leading plus sign
{"a": .5}                 // missing integer part
{"a": NaN}                // non-standard literal
{"a": undefined}          // not a JSON value
{"a": 1} // comment       // comment

กรณีขอบที่น่าสนใจ: {"a": 1} ตามด้วยคอมเมนต์ใช้ได้ใน JSON5 ใช้ได้ใน JSONC และผิดใน JSON มาตรฐาน ดังนั้น "ไฟล์คอนฟิกรันได้ปกติแต่ API ปฏิเสธ" แทบทุกครั้งคือปัญหาภาษาถิ่น

7. รายการตรวจสอบสำหรับโปรดักชัน

  • [ ] JSON ทุกชิ้นที่ส่งออกภายนอกใช้ application/json โดยไม่มี charset;
  • [ ] ไปป์ไลน์อ่าน/เขียนไฟล์ตัด BOM และใช้ UTF-8 อย่างชัดเจน;
  • [ ] จำนวนเต็มที่เกิน 2^53 ส่งเป็นสตริงและระบุหมายเหตุใน schema;
  • [ ] ตรวจจับคีย์ซ้ำตอนบิลด์;
  • [ ] เซิร์ฟเวอร์จำกัดขนาด request body และความลึกของการซ้อน;
  • [ ] ค่าระดับบนสุดของ API สาธารณะเป็น object หรือ array ไม่ใช่ค่าสเกลาร์เปล่า ๆ

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

จะรู้ได้เร็วอย่างไรว่าข้อความหนึ่งเป็น JSON ที่ถูกต้องหรือไม่

ส่งข้อความให้ตัวแยกวิเคราะห์หรือเครื่องมือแยกวิเคราะห์ออนไลน์ อ่านตำแหน่งของอักขระผิดกฎตัวแรก แล้วเทียบกับค่าหกประเภทและกฎช่องว่างของ RFC 8259 ระดับบนสุดเป็นค่า JSON ใดก็ได้ แต่ระหว่างองค์ประกอบเชิงโครงสร้างอนุญาตเฉพาะช่องว่าง แท็บ line feed และ carriage return เครื่องหมายคำพูดเดี่ยว คีย์ที่ไม่ครอบเครื่องหมายคำพูด เครื่องหมายจุลภาคท้ายรายการ หรือคอมเมนต์ แสดงว่าไม่ใช่ JSON มาตรฐาน

ทำไม JSON ต้องใช้ UTF-8 และไม่มี BOM

หมวด 8.1 ของ RFC 8259 กำหนดให้ข้อความ JSON ที่ใช้แลกเปลี่ยนข้อมูลเข้ารหัสเป็น UTF-8 บอกว่าการนำไปใช้งานต้องไม่เพิ่ม BOM ที่ต้นข้อความ และให้ตัวแยกวิเคราะห์เพิกเฉยต่อ BOM เพื่อความทนทาน ไฟล์ที่มี BOM จะถูก JSON.parse ปฏิเสธด้วย Unexpected token แม้สายตาจะไม่เห็นอะไรผิด ดังนั้นให้ตัด BOM ตอนอ่านและถอดรหัสเป็น UTF-8 อย่างชัดเจน

ควรใช้ JSON5 หรือ JSONC เพื่อหลีกเลี่ยงข้อจำกัดไวยากรณ์เหล่านี้หรือไม่

ได้ แต่เฉพาะการอ่านในเครื่องอย่างไฟล์คอนฟิกเท่านั้น; ข้อมูลที่แลกเปลี่ยนระหว่างระบบยังควรเป็น JSON มาตรฐาน ตัวอย่างที่ผิดแสดงว่า {"a": 1} ตามด้วยคอมเมนต์ใช้ได้ใน JSON5 และ JSONC แต่ผิดใน JSON มาตรฐาน ซึ่งเป็นเหตุให้ "ไฟล์คอนฟิกรันได้แต่ API ปฏิเสธ" แทบทุกครั้งเป็นปัญหาภาษาถิ่น เมื่อแปลง ให้ลบคอมเมนต์ เครื่องหมายคำพูดเดี่ยว และเครื่องหมายจุลภาคท้ายรายการก่อน แล้วส่งผลลัพธ์ให้ตัวแยกวิเคราะห์มาตรฐาน