Toolora

วิธีแก้ข้อผิดพลาดของ JSON ที่พบบ่อย

เก้าสาเหตุเรียงตามความถี่ที่พบจริง พร้อมเหตุผลว่าทำไมบรรทัดที่แจ้งมามักไม่ใช่จุดที่ผิดจริง

JSON error แทบทุกกรณีเกิดจากเก้าเรื่องนี้ รายการด้านล่างเรียงตามลำดับความถี่ที่มักเป็นสาเหตุจริง การไล่จากบนลงล่างจึงเป็นทางที่เร็วที่สุด

หากมี JSON ที่พังอยู่ตรงหน้าแล้ว ลองวางลงในเครื่องมือจัดรูปแบบ JSONก่อน เพราะมันบอกบรรทัดและคอลัมน์ที่ผิดให้ทันที ซึ่งช่วยตัดตัวเลือกส่วนใหญ่ออกได้ในไม่กี่วินาที

ก่อนอื่น: ตำแหน่งที่แจ้งคือจุดที่ parser ไปต่อไม่ไหว

เรื่องนี้ทำให้คนสับสนมากกว่ากฎไวยากรณ์ข้อไหน บรรทัดและคอลัมน์ในข้อความ error คือจุดที่ parser อ่านต่อไม่ได้แล้ว ไม่ใช่จุดที่คุณพิมพ์ผิดเสมอไป

{
  "a": 1,
  "b": 2

ความผิดคือลืมปิด } ซึ่งควรอยู่บรรทัดที่ 4 และ error ก็แจ้งว่า line 4 column 1 คือท้ายไฟล์พอดี ในตัวอย่างสั้น ๆ แบบนี้ตำแหน่งบังเอิญใกล้กัน แต่ถ้าเป็นไฟล์ยาว 500 บรรทัด วงเล็บที่ลืมปิดตั้งแต่บรรทัดที่ 12 ก็ยังจะถูกแจ้งที่บรรทัด 500 อยู่ดี

ดังนั้นเมื่อตำแหน่งที่แจ้งชี้ไปยังโค้ดที่ดูไม่มีอะไรผิด ความผิดจริงมักอยู่ก่อนหน้านั้น และเกือบทุกครั้งคือวงเล็บหรืออัญประกาศที่ยังไม่ได้ปิด

1. เครื่องหมายจุลภาคเกินท้ายรายการ

พบบ่อยที่สุด ทิ้งห่างข้ออื่นชัดเจน

{ "name": "Toolora", "free": true, }
{ "name": "Toolora", "free": true }

JavaScript ยอมให้มีจุลภาคท้ายรายการใน object และ array ได้ ทั้งความเคยชินและตัวช่วยในโปรแกรมแก้ไขโค้ดจึงผลักให้เราใส่มัน แต่ JSON ไม่ยอมรับในทุกกรณี ถ้ากำลังแก้ไฟล์ config ด้วยมือ ให้ตรวจข้อนี้ก่อนเสมอ

สังเกตว่า error ของสองกรณีต่างกัน จุลภาคเกินใน object จะได้ตำแหน่งมาด้วย แต่ใน array นั้น V8 แจ้งแค่ Unexpected token ']' โดยไม่มีตำแหน่งเลย นี่เป็นลักษณะเฉพาะของ engine ไม่ได้แปลว่าเป็นคนละปัญหา

2. ใช้อัญประกาศเดี่ยว หรือไม่ใส่อัญประกาศให้ key

{ 'name': 'Toolora' }
{ "name": "Toolora" }

JSON บังคับใช้ อัญประกาศคู่ เท่านั้น ทั้งกับ key และกับค่าที่เป็นข้อความ อัญประกาศเดี่ยวใช้ไม่ได้เด็ดขาด ส่วน key ที่ไม่ใส่อัญประกาศ ซึ่งเขียนได้ปกติใน JavaScript อย่าง {name: "x"} ก็ถูกปฏิเสธเช่นกัน

ทั้งสองกรณีให้ข้อความเดียวกันคือ Expected property name or '}'

3. คอมเมนต์

JSON ไม่มีรูปแบบคอมเมนต์เลย ทั้งสองแบบนี้ใช้ไม่ได้:

{
  // ชื่อที่แสดง
  "name": "Toolora"
}
{ /* ชื่อที่แสดง */ "name": "Toolora" }

ข้อนี้ทำให้หลายคนงง เพราะเครื่องมือจำนวนมากยอมรับคอมเมนต์ในไฟล์นามสกุล .json เครื่องมือเหล่านั้นใช้รูปแบบ JSONC หรือ JSON5 ซึ่งเป็นคนละมาตรฐานกัน ตัวอย่างที่เจอบ่อยคือ settings.json ของ VS Code และ tsconfig.json — แต่ parser ที่เคร่งครัดจะปฏิเสธไฟล์เดียวกันนั้น

หากจำเป็นต้องใส่หมายเหตุใน JSON ที่ parser เคร่งครัดจะอ่าน ให้เก็บไว้ใน key จริง ๆ แทน:

{ "_comment": "ชื่อที่แสดง", "name": "Toolora" }

4. NaN, Infinity และ undefined

ทั้งสามตัวไม่มีอยู่ใน JSON ค่าว่างที่รูปแบบนี้มีให้คือ null เท่านั้น

{ "score": NaN, "limit": Infinity, "note": undefined }
{ "score": null, "limit": null, "note": null }

กรณีนี้มักมาจากการประกอบข้อความ JSON ด้วยมือ หรือจากภาษาที่ชนิดข้อมูลทศนิยมมีค่า infinity อยู่จริง ควรตัดสินใจก่อนแปลงเป็น JSON ว่าแต่ละค่าควรกลายเป็นอะไร — null, 0, หรือตัด key นั้นทิ้งไปเลย

5. อักขระในข้อความที่ไม่ได้ใส่อักขระหลีก

ข้อความใน JSON มีการขึ้นบรรทัดใหม่แบบดิบ แท็บแบบดิบ อัญประกาศคู่ที่ไม่ได้หลีก หรือแบ็กสแลชโดด ๆ ไม่ได้

{ "path": "C:\Users\me" }
{ "path": "C:\\Users\\me" }

พาธของ Windows คือกรณีคลาสสิก แบ็กสแลชทุกตัวต้องเขียนซ้ำสองครั้ง การขึ้นบรรทัดใหม่แบบดิบจะแจ้งว่า Bad control character in string literal ส่วนแบ็กสแลชโดด ๆ จะแจ้งว่า Bad escaped character ทั้งสองแบบชี้ตรงไปที่อักขระที่มีปัญหา จึงแก้ได้ง่ายเมื่อรู้ความหมายของข้อความ

ข้อความหลายบรรทัดต้องใช้ \n ไม่ใช่การกดขึ้นบรรทัดใหม่จริง:

{ "note": "บรรทัดแรก\nบรรทัดที่สอง" }

6. วงเล็บผิดชนิดหรือปิดไม่ครบ

{ "items": [1, 2, 3} }
{ "items": [1, 2, 3] }

object ปิดด้วย } ส่วน array ปิดด้วย ] การใช้สลับกัน หรือนับชั้นผิดในข้อมูลที่ซ้อนกันหลายชั้น จะได้ error ที่ตำแหน่งห่างจากจุดผิดจริงมาก ตามกฎที่อธิบายไว้ข้างบน

วิธีที่เร็วที่สุดคือจัดรูปแบบเอกสารเสียก่อน เพราะการย่อหน้าจะทำให้เห็นชั้นที่ปิดไม่ครบได้ทันทีด้วยตาเปล่า

7. อักขระที่มองไม่เห็นอยู่ต้นไฟล์

หาก error ชี้ไปที่อักขระตัวแรกสุดของไฟล์ที่ดูถูกต้องทุกอย่าง มักเป็นเพราะ byte order mark ซึ่งเป็นอักขระ U+FEFF ที่มองไม่เห็น โปรแกรมแก้ไขข้อความบน Windows บางตัวเขียนมันไว้ต้นไฟล์ UTF-8 และมันติดมากับการคัดลอกจากบางแหล่งด้วย

โปรแกรมแก้ไขจะไม่แสดงอะไรให้เห็นเลย แต่ parser จะแจ้งว่า Unexpected token วิธีแก้คือบันทึกไฟล์เป็น “UTF-8 without BOM” หรือลบแล้วพิมพ์วงเล็บเปิดใหม่

หลักเดียวกันนี้ใช้กับช่องว่างแบบไม่ตัดคำ (non-breaking space) และอักขระที่มองไม่เห็นอื่น ๆ ที่ติดมาจากการคัดลอกจากหน้าเว็บหรือเอกสาร

8. ตัวเลขใหญ่เกินกว่าจะรอดการแปลงไปกลับ

ข้อนี้ไม่แจ้ง error ใด ๆ เลย จึงอันตรายที่สุดในรายการนี้

ตัวเลขใน JavaScript เป็นแบบ IEEE-754 double จำนวนเต็มที่มากกว่า 9,007,199,254,740,991 (Number.MAX_SAFE_INTEGER) จึงเก็บค่าที่แม่นยำไม่ได้ JSON.parse จะรับค่าเข้าไปแล้วคืนค่าที่เพี้ยนกลับมาอย่างเงียบ ๆ

{ "id": 1234567890123456789 }

เมื่อผ่าน JSON.parse แล้วแปลงกลับเป็นข้อความใน JavaScript ค่านั้นจะกลายเป็น 1234567890123456800 — ID ผิดไปแล้ว ไม่มี error ใด ๆ เกิดขึ้น และไม่มีอะไรใน log บอกคุณด้วย

ID ขนาดใหญ่ เช่น snowflake ของ Discord, ID ของ Twitter/X หรือ bigint จากฐานข้อมูล ควรส่งเป็น ข้อความ แทน:

{ "id": "1234567890123456789" }

หากคุณเป็นเจ้าของ API เอง ควรแก้ที่ต้นทาง ดีกว่าไปไล่แก้ที่ผู้ใช้ปลายทางทุกราย

9. ลำดับของ key ไม่ได้ถูกรับประกัน

มาตรฐาน JSON นิยาม object ว่าเป็นกลุ่มของคู่ชื่อกับค่าที่ไม่มีลำดับ ในทางปฏิบัติ parser ส่วนใหญ่รักษาลำดับที่คุณเขียนไว้ แต่ก็ไม่เสมอไป และ JavaScript มีข้อยกเว้นหนึ่งที่ควรรู้

key ที่หน้าตาเหมือนจำนวนเต็มไม่ติดลบจะถูกจัดเรียงใหม่ตามค่าตัวเลข แล้วย้ายไปอยู่ข้างหน้าสุด:

{ "b": 1, "a": 2, "2": 3, "1": 4 }

เมื่อผ่าน JSON.parse แล้ว JSON.stringify กลับมา จะได้ {"1":4,"2":3,"b":1,"a":2} นี่เป็นกฎของ object ใน JavaScript ไม่ใช่ของตัว parser และมีผลกับทุก object ที่มี key เป็นตัวเลข

หากลำดับมีความสำคัญ เช่น ลำดับขั้นตอน หรือรายการที่จัดอันดับไว้ ให้ใช้ array แทน เพราะ array ถูกออกแบบมาเพื่อการนี้และรับประกันลำดับให้

วิธีแก้

วางเอกสารลงในเครื่องมือจัดรูปแบบ JSON มันจะบอกบรรทัดและคอลัมน์ที่ผิดให้ และการจัดรูปแบบ JSON ที่ถูกต้องด้วยการย่อหน้าที่สม่ำเสมอจะทำให้เห็นปัญหาเชิงโครงสร้างได้ทันที ทุกอย่างทำงานในเบราว์เซอร์ของคุณ ไฟล์ config ที่มีรหัสผ่านอยู่ข้างในจึงไม่ถูกส่งออกจากเครื่องเลย