JSON5 และ JSONC: JSON ที่มี Comment
สรุปสำคัญ: ทั้งสองภาษาถิ่นมีอยู่เพราะ config ที่มนุษย์เขียนต้องมี comment และ trailing comma แต่ไวยากรณ์และการเข้าถึงระบบนิเวศต่างกัน เลือกผิด toolchain ก็ติดขัด

สรุปสั้น: ใช้ JSONC สำหรับ comment ใน configuration ของ VS Code และ TypeScript, ใช้ JSON5 เมื่อต้องใช้ไวยากรณ์ literal ของ ES5.1 ที่กว้างกว่า เช่นเครื่องหมายคำพูดเดี่ยวหรือคีย์ที่ไม่ใส่เครื่องหมายคำพูด และเก็บ API สาธารณะไว้บน JSON มาตรฐาน
JSON มาตรฐานไม่อนุญาต comment ทุกทีมจึงเขียนมันอยู่เงียบ ๆ ต่อไป — จนวันหนึ่ง build พัง แทนที่จะทำอย่างลับ ๆ ให้เลือกภาษาถิ่นที่เหมาะสม
1. สองภาษาถิ่นมีแรงจูงใจต่างกัน
- JSONC (JSON with Comments): JSON มาตรฐานบวก comment และ trailing comma โดยแทบไม่เปลี่ยนไวยากรณ์อื่น มันเป็นมาตรฐานโดยพฤตินัยของระบบนิเวศ VS Code —
settings.json,tsconfig.jsonและ.eslintrc.jsonต่างใช้มัน - JSON5: ทะเยอทะยานกว่ามาก สอดคล้องโดยตรงกับ object literal ของ ES5.1 — เครื่องหมายคำพูดเดี่ยว, คีย์ที่ไม่ใส่เครื่องหมายคำพูด, ฐานสิบหก,
Infinity/NaN, การต่อบรรทัดด้วย backslash และเครื่องหมาย+นำหน้า
หนึ่งประโยคเพื่อแยกทั้งสอง: JSONC เพียงทำให้การเขียนสบายขึ้น; JSON5 ให้คุณเขียนไฟล์กำหนดค่าเสมือนเป็น JavaScript
2. ตารางเปรียบเทียบไวยากรณ์
| ความสามารถ | JSON มาตรฐาน | JSONC | JSON5 |
|---|---|---|---|
Comment // /* */ |
✗ | ✓ | ✓ |
| Trailing comma | ✗ | ✓ | ✓ |
| String ที่ใช้เครื่องหมายคำพูดเดี่ยว | ✗ | ✗ | ✓ |
| คีย์ที่ไม่ใส่เครื่องหมายคำพูด | ✗ | ✗ | ✓ |
| จำนวนฐานสิบหก | ✗ | ✗ | ✓ |
จุดทศนิยมนำหน้าหรือต่อท้าย .5 5. |
✗ | ✗ | ✓ |
จำนวนที่เริ่มด้วย + |
✗ | ✗ | ✓ |
Infinity / NaN |
✗ | ✗ | ✓ |
การต่อบรรทัดด้วย \ ท้ายบรรทัด |
✗ | ✗ | ✓ |
| String หลายบรรทัด | ✗ | ✗ | ✓ (ทำผ่านการต่อบรรทัด) |
| ค่าระดับบนสุดอะไรก็ได้ | ✓ | ✓ | ✓ |
สเปก JSON5 ทั้งหมดยาวกว่าหนึ่งหน้าเพียงเล็กน้อย; แพ็กเกจ json5 ได้รับการดูแลโดยชุมชนมายาวนาน และ browser กับ Node มีพฤติกรรมเหมือนกัน
3. การ parse: สอง API สองระดับความสามารถในการระบุตำแหน่ง
3.1 JSON5: parse ทั้งเอกสารอย่างง่าย
import JSON5 from 'json5';
const config = JSON5.parse(`
{
// service port
port: 8080,
host: '0.0.0.0',
features: ['json5', 'jsonc',],
retries: 0x10,
}
`);
console.log(config.port, config.retries); // 8080 16
JSON5.parse คืน object ธรรมดาที่ไม่มีข้อมูล offset — เพราะมันไม่ได้พุ่งเป้าสถานการณ์แก้ไข แต่พุ่งเป้า "การอ่าน configuration" เท่านั้น
3.2 JSONC: syntax tree ที่มี offset
คุณค่าของ jsonc-parser ไม่ได้อยู่ที่การ parse แต่อยู่ที่ การแก้ไข มันมี API สามระดับ:
| API | จุดประสงค์ | การใช้งานทั่วไป |
|---|---|---|
parse |
Parse เป็น tree ที่พา offset/length |
ไฮไลต์ไวยากรณ์, folding, การนำทาง |
visit |
เดินแบบ depth-first; callback รับ path ของ node | การตรวจสอบคล้าย JSONPath |
modify + applyEdits |
สร้างการแก้ไขข้อความขั้นต่ำ แล้วนำไปใช้กับข้อความต้นฉบับ | เปลี่ยนค่าโดยรักษา comment |
import { parseTree, findNodeAtLocation, modify, applyEdits } from 'jsonc-parser';
const text = `{
// port configuration
"server": {
"port": 8080 // 80 in production
}
}`;
const tree = parseTree(text);
const portNode = findNodeAtLocation(tree!, ['server', 'port']);
console.log(portNode?.offset, portNode?.length);
const edits = modify(text, ['server', 'port'], 3000, {
formattingOptions: { insertSpaces: true, tabSize: 2 },
});
console.log(applyEdits(text, edits));
เอาต์พุตเก็บ comment เดิมไว้และแทนที่เพียงสามอักขระของ 8080:
{
// port configuration
"server": {
"port": 3000 // 80 in production
}
}
นั่นคือสิ่งที่ "แก้ไข configuration ด้วย regular expression" ทำไม่ได้: regex ไม่รู้ว่า 8080 ตัวไหนควรเปลี่ยน และไม่รู้ว่า 8080 ที่อยู่ใน string ต้องปล่อยไว้
3.3 ข้อความ error ต้องพา offset
error code ของ jsonc-parser เป็นค่า enum ตัวที่พบบ่อยคือ:
| Error code | ความหมาย |
|---|---|
InvalidSymbol |
มีอักขระที่ไม่ควรอยู่ปรากฏขึ้น |
PropertyNameExpected |
คาดว่าจะเจอชื่อคีย์ |
ColonExpected |
คาดว่าจะเจอ colon |
CommaExpected |
คาดว่าจะเจอเครื่องหมายจุลภาค |
CloseBraceExpected |
object ไม่ถูกปิด |
UnexpectedEndOfComment |
comment ไม่ถูกปิด |
InvalidEscapeCharacter |
escape ไม่ถูกต้อง |
เมื่อแปลง offset เป็นบรรทัดและคอลัมน์ได้ editor ก็วาดเส้นหยักที่ตำแหน่งผิดพลาดได้พอดี
4. ใช้แบบไหนเมื่อใด
| สถานการณ์ | คำแนะนำ | เหตุผล |
|---|---|---|
| body ของ request/response API สาธารณะ | JSON มาตรฐาน | ไคลเอนต์ทั่วไปไม่รองรับภาษาถิ่น |
package.json, composer.json |
JSON มาตรฐาน | package manager เข้าใจเพียง JSON มาตรฐาน |
tsconfig.json, .vscode/settings.json |
JSONC | ค่าเริ่มต้นของระบบนิเวศ รองรับโดยธรรมชาติใน toolchain |
| Configuration ของแอป (ต้องมี comment) | JSONC หรือ YAML | ต้องรักษา comment ขณะที่เครื่องมือยังอ่านและเขียนได้ |
ต้องใช้ Infinity, ฐานสิบหก, literal ซับซ้อน |
JSON5 | พลังในการแสดงออกสูงสุด |
| การแลกเปลี่ยนข้อมูล, log, แคช | รูปแบบคั่นด้วยบรรทัดหรือ JSON มาตรฐาน | ต้องมี streaming และความเป็นสากล |
กฎง่าย ๆ: หากไฟล์จะถูกอ่านโดย "เครื่องมือที่ไม่ใช่มนุษย์" ให้เลือก JSON มาตรฐาน; พิจารณาภาษาถิ่นเฉพาะเมื่อมั่นใจว่าโค้ดของคุณเองเท่านั้นที่อ่านมัน
5. ความเสี่ยงสามประเภทที่ภาษาถิ่นนำมา
5.1 ความต้องการล้ำขอบเขต
เมื่อเครื่องมืออื่นอ่านไฟล์กำหนดค่า JSON5 แล้วเรียก JSON.parse มัน crash ทันที อาการทั่วไปคือ "ในเครื่องใช้ได้ แต่บน Docker พัง" — เพราะ container รันเส้นทางการอ่านที่ต่างออกไป
มาตรการรับมือ: เพิ่มการตรวจใน CI ของ repository ที่ระบุชัดเจนว่าไฟล์ใดอนุญาตให้ใช้ภาษาถิ่น; ที่เหลือตรวจเป็น JSON มาตรฐาน
5.2 เซอร์ไพรส์เรื่องการอนุมานชนิด
JSON5 อนุญาตคีย์ที่ไม่ใส่เครื่องหมายคำพูดและเครื่องหมายคำพูดเดี่ยว แต่ ไม่ ให้คุณนำโครงสร้าง literal อย่างวันที่หรือ regular expression เข้ามาได้ — มันยังเป็นรูปแบบข้อมูล ไม่ใช่การประเมิน JavaScript
มีคนเคยคิดว่า JSON5 แสดง new Date() ได้ แล้วพบว่า parse ล้มเหลว JSON5 ไม่ใช่ eval
5.3 Comment ถูกทิ้งอย่างเงียบ ๆ
ทั้ง JSON5.stringify และ JSON.stringify ไม่รักษา comment ดังนั้น "อ่านเข้ามา ปรับค่า แล้วเขียนกลับ" จะสูญเสีย comment ทั้งหมด
เมื่อ comment ต้องอยู่รอด ทางเลือกเดียวคือ editing API ของ jsonc-parser หรือไลบรารี YAML ที่รองรับการรักษา comment
6. คำแนะนำในทางปฏิบัติ
- [ ] เก็บตาราง "ชนิดไฟล์ → รูปแบบ" ในเอกสารของทีม เพื่อไม่ให้ใครทำอะไรตามใจ;
- [ ] ห่อการอ่าน configuration ไว้ใน
loadConfig()ตัวเดียวที่ตัดสินใจเองว่าจะใช้ parser ใด โค้ดธุรกิจจึงไม่เรียกJSON.parseโดยตรง; - [ ] เมื่อแก้ไข configuration ที่มี comment โดยโปรแกรม ให้ใช้
modify+applyEditsอย่าเขียนไฟล์ใหม่ทั้งหมด; - [ ] ตรวจไฟล์ภาษาถิ่นใน CI ด้วย (
jsonc-parserสำหรับ JSONC,JSON5.parseสำหรับ JSON5) ไม่อย่างนั้น syntax error จะปรากฏเฉพาะตอนรัน; - [ ] ใช้ JSON มาตรฐานสำหรับอินเทอร์เฟซสาธารณะ, คีย์แคช และ payload ของ message queue
คำถามที่พบบ่อย
ควรเลือกระหว่าง JSON5 กับ JSONC อย่างไร?
เลือกตาม runtime ไม่ใช่ตามจำนวนไวยากรณ์: VS Code, TypeScript และ ESLint อ่าน JSONC เป็นค่าเริ่มต้น JSONC จึงให้ความเข้ากันได้ดีที่สุด; ใช้ JSON5 เฉพาะเมื่อต้องใช้ไวยากรณ์ literal ของ ES5.1 ที่กว้างกว่า เช่นเครื่องหมายคำพูดเดี่ยว คีย์ที่ไม่ใส่เครื่องหมายคำพูด หรือ Infinity ทั้งสองภาษาถิ่นไม่เหมาะกับ response ของ API สาธารณะ
ทำไม parser JSON มาตรฐานอ่าน JSONC โดยตรงไม่ได้?
JSONC อนุญาต comment และ trailing comma ขณะที่ JSON.parse รับเฉพาะ JSON ตาม RFC 8259 และโยน error กับทั้งสองอย่าง การอ่าน JSONC ต้องใช้ parser เช่น jsonc-parser หรือลบ comment ก่อนส่งข้อความให้ parser มาตรฐาน
Comment ใน JSONC ควรอยู่รอดหลังแก้ไขไฟล์หรือไม่?
ใช่ แต่ต้องผ่าน editing API เท่านั้น modify และ applyEdits ของ jsonc-parser เปลี่ยน node เป้าหมายโดยรักษา comment ที่เหลือ; การ parse เป็น object แล้ว stringify จะทิ้ง comment ทั้งหมด