خواندن مرزهای دستور زبان JSON از RFC 8259
جمعبندی: ترجمه تولیدهای دستور زبان RFC 8259 به زبان ساده: کدام کاراکترها را میتوان مستقیم نوشت، کدامها باید escape شوند و استاندارد کجا آگاهانه دست باز گذاشته است.

جمعبندی: وقتی خطای نحوی JSON ظاهر میشود، نخستین کاراکتر غیرمجاز را پیدا کنید و آن را در یکی از دستههای تکگیومه، ویرگول انتهایی، کامنت یا کاراکتر کنترلی بگذارید، نه اینکه کل سند را دوباره بخوانید؛ در سمت رمزگذاری هم UTF-8 بدون BOM را تأیید کنید و عددهای صحیح بزرگتر از 2^53 - 1 را بهصورت رشته حمل کنید.
متن استاندارد تنها دوازده صفحه است، اما پشت هر «MUST»، «SHOULD» و «MAY» حادثهای پنهان است که روزی یک تیم را تا صبح بیدار نگه داشته.
۱. سه متن چگونه به هم مربوطاند
یادگیری JSON اغلب بهخاطر سه شماره گیجکننده میشود: RFC 4627، RFC 7159، RFC 8259 و همچنین ECMA-404. رابطه آنها در واقع کاملاً روشن است:
| متن | تاریخ | وضعیت | تفاوت کلیدی |
|---|---|---|---|
| RFC 4627 | ۲۰۰۶ | جایگزین شده | سطح بالا باید object یا array باشد |
| RFC 7159 | ۲۰۱۴ | جایگزین شده | سطح بالا به هر مقدار JSON تسهیل شد |
| RFC 8259 | ۲۰۱۷.۱۲ | استاندارد کنونی | UTF-8 صریح، راهنمای همکنشپذیری افزوده شد |
| ECMA-404 | ۲۰۱۳، ویرایش دوم ۲۰۱۷.۱۲ | مشخصات دستور زبان | فقط دستور زبان را تعریف میکند و رمزگذاری و انتقال را پوشش نمیدهد |
کدام را باید خواند؟ تنها RFC 8259 کافی است. بخش دستور زبان ECMA-404 معادل است؛ تفاوت در این است که RFC 8259 همچنین «انتقال بهصورت UTF-8» و «مسائل همکنشپذیری که پیادهسازان باید مراقب آن باشند» را مشخص میکند.
۲. شش نوع مقدار
تولیدهای دستور زبان استاندارد را میتوان در یک نمودار خلاصه کرد:
JSON-text = ws value ws
value = object / array / number / string / "true" / "false" / "null"
object = "{" [ member *( "," member ) ] "}"
member = string ws ":" ws value
array = "[" [ value *( "," value ) ] "]"
فقط شش نکته کلیدی وجود دارد:
۱. Objectها از آکولاد استفاده میکنند، عضوها با ویرگول جدا میشوند و ویرگول پس از آخرین عضو مجاز نیست؛
۲. Arrayها از کروشه استفاده میکنند، عناصر با ویرگول جدا میشوند و ویرگول انتهایی نیز به همین شکل مجاز نیست؛
۳. عددها علامت + ابتدایی، صفر ابتدایی (01 نامعتبر است)، نقطه اعشار تنها (1. نامعتبر است) و NaN یا Infinity را نمیپذیرند؛
۴. رشتهها باید از گیومه دوتایی استفاده کنند؛ تکگیومه نامعتبر است؛
۵. بولینها و null لیترالهای حروف کوچک هستند؛ True و NULL هر دو نامعتبرند؛
۶. تنها کاراکترهای فاصله 0x20 (فاصله)، 0x09 (تب)، 0x0A (خط جدید) و 0x0D (بازگشت به ابتدای خط) میتوانند میان عناصر ساختاری ظاهر شوند.
قطعه زیر JSON معتبر است، هرچند بسیاری از افراد نخستین بار که آن را میبینند گمان میکنند اشتباه است — چون یک رشته سطح بالا است:
"just a string"
از RFC 7159 به بعد، سطح بالا واقعاً میتواند هر مقداری باشد. با این حال در APIها هنوز توصیه میشود فقط از object یا array استفاده کنید؛ دلیلش در بخش ۶ آمده است.
۳. واقعاً چه چیزی میتواند مستقیماً درون رشته ظاهر شود
این بخشی است که بیش از همه با «روی سیستم من درست کار میکند» فریب میخورد. قواعد استاندارد برای رشتهها چنیناند:
- کاراکترهای مجاز برای ظهور مستقیم:
%x20-21،%x23-5B،%x5D-10FFFF؛ - به بیان دیگر، گیومه دوتایی
"(0x22) و بکاسلش\(0x5C) باید escape شوند؛ - همه کاراکترهای کنترلی زیر 0x20 باید escape شوند، از جمله خط جدید، تب و بازگشت به ابتدای خط؛
- تنها دنبالههای escape موجود عبارتاند از
\"،\\،\/،\b،\f،\n،\r،\tو\uXXXX.
{
"ok": "متن چینی، emoji 🎉 و نمادهای تمامعرض را میتوان مستقیم نوشت",
"escaped": "اینجا یک خط جدید میآید\nو یک تب\tآن را تمام میکند",
"control": "\u0007 کاراکتر زنگ است و باید escape شود",
"quote": "او گفت: \"از تکگیومه استفاده نکن\""
}
\/ یک escape قدیمی است (جلوگیری میکند از اینکه </script> زودتر بسته شود). سناریوهای امروزی بهندرت به آن نیاز دارند، اما تجزیهکنندهها باید آن را پشتیبانی کنند.
۴. رمزگذاری: UTF-8، بدون BOM
بخش ۸.۱ RFC 8259 سه چیز میگوید:
۱. متن JSON که برای تبادل استفاده میشود باید با UTF-8 رمزگذاری شود؛
۲. پیادهسازیها نباید در ابتدای متن BOM (EF BB BF) اضافه کنند؛
۳. تجزیهکنندهها میتوانند برای استحکام بیشتر BOM را نادیده بگیرند.
دو شکست معمول در دنیای واقعی:
- Notepad ویندوز فایل را با «UTF-8 with BOM» ذخیره میکند و
JSON.parseفایل را باUnexpected tokenرد میکند؛ - یک فایل JSON چینی که با GBK ذخیره شده، در خط لولهای که فقط UTF-8 رمزگشایی میکند به متن آشوب تبدیل میشود، با این حال نحو بهطور تصادفی عبور میکند و خطا به لایه کسبوکار موکول میشود.
راهحل ساده است: هنگام خواندن فایل 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 را نمیپذیرد؛ نوشتن آن خطا ایجاد نمیکند، اما رویهای منسوخ است.
۵. سه جایی که استاندارد آگاهانه دست باز گذاشته است
جاهایی که استاندارد از تعیین تکلیف خودداری میکند، دقیقاً همان جاهایی هستند که تفاوت پیادهسازیها از آنها میآید.
۱. بازه و دقت اعداد
RFC 8259 فقط میگوید «این مشخصات بازه یا دقت اعداد را محدود نمیکند» و یادآوری میکند که بیشتر پیادهسازیها از دقت مضاعف IEEE 754 استفاده میکنند.
این یعنی همان 9223372036854775807 در JavaScript به 9223372036854776000 تبدیل میشود، اما در long جاوا دقیق باقی میماند.
نتیجه: عددهای صحیح بزرگتر از 2^53 - 1 و مبلغهای پولی که به دقت اعشاری زیاد نیاز دارند، همیشه باید بهصورت رشته حمل شوند و pattern صریحاً در schema نوشته شود.
۲. کلیدهای تکراری
عبارت استاندارد این است که «نامهای درون یک object باید (SHOULD) یکتا باشند»؛ اما نمیگوید وقتی یکتا نیستند چه باید کرد.
JSON.parseآخرین را نگه میدارد؛- برخی تجزیهکنندهها اولین را نگه میدارند؛
- برخی کتابخانهها بهسادگی خطا میدهند.
نتیجه: کلید تکراری ننویسید و در اعتبارسنجها به هیچ رفتار خاصی تکیه نکنید. افزودن یک مرحله «شناسایی کلید تکراری» در زمان ساخت بسیار ارزانتر از بررسی پس از وقوع است.
۳. محدودیت عمق و طول
استاندارد هیچ محدودیتی برای عمق تودرتویی تعیین نمیکند، اما هر پیادهسازی یکی دارد. V8 در دهها هزار سطح تودرتویی RangeError: Maximum call stack size exceeded میاندازد.
سرورها باید محدودیت خودشان را اعمال کنند؛ این یک الزام امنیتی است، نه الزام استاندارد.
۶. آزمایش درک با مثالهای نقض
هر خط زیر JSON نامعتبر است. آنها را به یک تجزیهکننده بدهید و ببینید پیامهای خطا چقدر راهگشا هستند:
{ '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 آن را رد میکند» تقریباً همیشه یک مسئله گویش است.
۷. چکلیست محیط عملیاتی
- [ ] همه JSONهای ارسالی به بیرون از
application/jsonبدونcharsetاستفاده میکنند؛ - [ ] خط لوله خواندن/نوشتن فایل BOM را حذف میکند و از UTF-8 صریح استفاده میکند؛
- [ ] عددهای صحیح بزرگتر از
2^53بهصورت رشته حمل و در schema حاشیهنویسی میشوند؛ - [ ] کلیدهای تکراری در زمان ساخت شناسایی میشوند؛
- [ ] سرور اندازه بدنه درخواست و عمق تودرتویی را محدود میکند؛
- [ ] مقدارهای سطح بالای APIهای عمومی object یا array هستند و هرگز اسکالر خالی نیستند.
پرسشهای متداول
چگونه سریع تشخیص دهم یک متن JSON معتبر است؟
متن را به یک تجزیهکننده یا ابزار تجزیه آنلاین بسپارید، موقعیت نخستین کاراکتر غیرمجاز را بخوانید و سپس آن را با شش نوع مقدار و قواعد فاصله RFC 8259 مقایسه کنید. سطح بالا میتواند هر مقدار JSON باشد، اما میان عناصر ساختاری فقط فاصله، تب، خط جدید و بازگشت به ابتدای خط مجاز است. تکگیومه، کلید بدون گیومه، ویرگول انتهایی یا کامنت یعنی JSON استاندارد نیست.
چرا JSON باید UTF-8 باشد و BOM نداشته باشد؟
بخش ۸.۱ RFC 8259 ایجاب میکند متن JSON مورد استفاده برای تبادل با UTF-8 رمزگذاری شود، میگوید پیادهسازیها نباید در ابتدا BOM اضافه کنند و به تجزیهکنندهها اجازه میدهد برای استحکام BOM را نادیده بگیرند. فایلی با BOM توسط JSON.parse با Unexpected token رد میشود، حتی اگر از نظر ظاهری چیزی اشتباه به نظر نرسد. پس هنگام خواندن BOM را حذف کنید و صریحاً با UTF-8 رمزگشایی کنید.
آیا برای دور زدن این محدودیتهای دستور زبان از JSON5 یا JSONC استفاده کنم؟
بله، اما فقط برای خواندن محلی مانند فایلهای پیکربندی؛ دادهای که میان سیستمها تبادل میشود باید همچنان JSON استاندارد باشد. مثالهای نقض نشان میدهند که {"a": 1} بههمراه کامنت در JSON5 و JSONC معتبر است اما در JSON استاندارد نامعتبر، و همین دلیل آن است که «فایل پیکربندی اجرا میشود اما API آن را رد میکند» تقریباً همیشه مسئله گویش است. هنگام تبدیل، نخست کامنتها، تکگیومهها و ویرگولهای انتهایی را حذف کنید و نتیجه را به یک تجزیهکننده استاندارد بسپارید.