JSON Toolkit

پرسش‌های متداول نحو و قالب: کامنت، ویرگول انتهایی و دقت

جمع‌بندی: آیا کامنت مجاز است، چرا ویرگول انتهایی خطا می‌دهد، آیا کلیدها به گیومه نیاز دارند، چرا 9007199254740993 تغییر می‌کند — دوازده نتیجه که هر یک بر اساس RFC 8259 بررسی شده است.

مقدماتیحدود 12 دقیقه مطالعهانتشار 2026-10-30به‌روزرسانی 2026-10-30
تصویر جلد پرسش‌های متداول نحو و قالب

جمع‌بندی: برای هر چیزی که از مرز یک فرایند عبور می‌کند JSON ساده RFC 8259 بنویسید — گیومه دوتایی، بدون کامنت، بدون ویرگول انتهایی، UTF-8 بدون BOM؛ برای پیکربندی‌ای که کامنت لازم دارد از JSONC یا JSON5 استفاده کنید و در زمان اجرا به JSON استاندارد برگردانید؛ و عددهای صحیح بالاتر از 2^53-1 را در سرور به‌صورت رشته سریالایز کنید.

وجه مشترک این پرسش‌ها این است که به نظر می‌رسد «باید کار کنند»، اما استاندارد در واقع بسیار سخت‌گیر است.

۱. چه چیزی JSON معتبر است

تفاوت JSON، JSON5 و JSONC چیست؟

JSON استاندارد فقط رشته‌های گیومه دوتایی را می‌شناسد و نه کامنت می‌پذیرد نه ویرگول انتهایی. JSON5 و JSONC هر دو ویژگی‌هایی روی آن اضافه می‌کنند:

ویژگی JSON (RFC 8259) JSONC JSON5
رشته‌های گیومه دوتایی پشتیبانی پشتیبانی پشتیبانی
رشته‌های تک‌گیومه پشتیبانی نمی‌شود پشتیبانی نمی‌شود پشتیبانی می‌شود
کامنت پشتیبانی نمی‌شود پشتیبانی می‌شود پشتیبانی می‌شود
ویرگول انتهایی پشتیبانی نمی‌شود پشتیبانی نمی‌شود پشتیبانی می‌شود
کلیدهای بی‌گیومه پشتیبانی نمی‌شود پشتیبانی نمی‌شود پشتیبانی می‌شود
عدد هگزادسیمال پشتیبانی نمی‌شود پشتیبانی نمی‌شود پشتیبانی می‌شود
NaN / Infinity پشتیبانی نمی‌شود پشتیبانی نمی‌شود پشتیبانی می‌شود
پسوند فایل .json .jsonc .json5

اصل انتخاب ساده است: همیشه در عبور از مرز فرایندها از JSON استاندارد استفاده کنید، در حالی که فایل‌های پیکربندی می‌توانند بسته به پشتیبانی ویرایشگر از JSONC یا JSON5 استفاده کنند. tsconfig.json و .vscode/settings.json می‌توانند کامنت داشته باشند چون ویرایشگر آن‌ها را JSONC می‌خواند؛ دادن آن‌ها به JSON.parse سخت‌گیرانه همچنان خطا می‌دهد.

آیا JSON می‌تواند کامنت داشته باشد؟

نه. دستور زبان RFC 8259 هیچ تولید کامنتی ندارد، پس هر // یا /* */ تجزیه را شکست می‌دهد.

اگر می‌خواهید توضیحات را در پیکربندی نگه دارید، سه جایگزین وجود دارد:

{
  "_comment": "以下超时单位是毫秒",
  "timeout": 3000,
  "retry": {
    "$doc": "重试次数,0 表示不重试",
    "count": 3
  }
}
  • از فیلدهای قراردادی مانند _comment استفاده کنید که خواننده صرفاً نادیده می‌گیرد؛
  • از فیلدهای با پیشوند $ استفاده کنید تا احتمال برخورد با فیلدهای کسب‌وکار کم شود؛
  • صرفاً برای پیکربندی به JSONC یا YAML روی آورید و در زمان اجرا به JSON استاندارد تبدیل کنید.

چرا ویرگول انتهایی خطا می‌دهد؟

چون در دستور زبان هم آرایه‌ها و هم اشیا ساختار «عنصر + جداکننده» دارند و پس از آخرین عنصر ویرگول مجاز نیست.

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

قطعه بالا یک ویرگول اضافه پیش از } دارد، و خطا بسته به پیاده‌سازی پارسر معمولاً روی ویرگول یا روی آکولاد بسته گزارش می‌شود. این همچنین آسان‌ترین تله هنگام مهاجرت از جاوااسکریپت است — object literalهای JS ویرگول انتهایی را می‌پذیرند، JSON نمی‌پذیرد.

آیا تک‌گیومه مجاز است؟

نه. رشته‌ها باید از گیومه دوتایی استفاده کنند؛ 'text' خطای نحوی است.

اگر داده از repr() پایتون، اسکریپت شل یا پیکربندی دست‌نویس کپی شده، اول با یک ابزار تبدیلش کنید نه اینکه امیدوار باشید پارسر سهل‌گیر باشد.

آیا کلیدها می‌توانند بی‌گیومه باشند؟

نه. نام اعضای شیء باید رشته‌ای در گیومه دوتایی باشد.

{ name: "json" }

این یک object literal جاوااسکریپت است، نه JSON. افزودن گیومه معتبرش می‌کند:

{ "name": "json" }

آیا سطح بالا می‌تواند آرایه یا رشته باشد؟

بله. RFC 8259 هر مقدار JSON را در سطح بالا مجاز می‌داند: اشیا، آرایه‌ها، رشته‌ها، عددها، true، false و null همه معتبرند.

اما در عمل مهندسی توصیه می‌شود در سطح بالا فقط از شیء یا آرایه استفاده شود، به دو دلیل:

  1. سازگاری: پارسرهای قدیمی (دوران RFC 4627) سطح بالا را شیء یا آرایه می‌خواهند؛
  2. امنیت: آرایه سطح بالا از نظر تاریخی ریسک JSON hijacking داشته است. مرورگرهای مدرن آن را رفع کرده‌اند، اما سیستم‌های قدیمی همچنان قرارداد را فرض می‌کنند.

پس "hello" JSON معتبر است و ["a", "b"] هم، اما API‌ای که {"data": [...]} برمی‌گرداند امن‌تر است.

۲. رمزگذاری و رشته‌ها

آیا BOM در ابتدای فایل تجزیه را می‌شکند؟

بله. RFC 8259 صریحاً الزام می‌کند متن JSON به‌صورت UTF-8 منتقل شود و نباید نشانه ترتیب بایت (BOM) اضافه شود.

BOM همان سه بایت EF BB BF در ابتدای فایل است. کاملاً نامرئی است، اما نخستین موقعیت رشته را اشغال می‌کند، پس پارسر نتیجه می‌گیرد «نخستین نویسه { نیست» و خطا می‌دهد — و خط و ستون گزارش‌شده معمولاً خط ۱، ستون ۱ است که گیج‌کننده به نظر می‌رسد.

«Save as UTF-8» در Notepad ویندوز به‌صورت پیش‌فرض BOM دارد. برای بررسی و حذف:

# Check: output starting with efbbbf means a BOM is present
head -c 3 data.json | xxd

# Remove: skip the first 3 bytes with tail
tail -c +4 data.json > clean.json

JSON.parse جاوااسکریپت مدرن BOM را تحمل می‌کند (V8 در داخل آن را رد می‌کند)، اما پیاده‌سازی‌های سخت‌گیر در Java، Go و Python نمی‌کنند، که دلیل رایجی است برای اینکه «همان فایل در زبان‌های مختلف نتایج متفاوت می‌دهد».

آیا نویسه‌های چینی باید به \uXXXX تبدیل شوند؟

نه. RFC 8259 متن JSON را UTF-8 می‌خواهد، پس چینی می‌تواند مستقیم ظاهر شود؛ {"name": "张三"} کاملاً معتبر است.

\uXXXX صرفاً یک املای معادل جایگزین است که برای کانال‌هایی استفاده می‌شود که نمی‌توانند نویسه را مستقیم نمایش دهند. قاعده سرانگشتی:

  • بدنه درخواست HTTP، فایل‌ها، پایگاه‌داده: چینی را مستقیم بنویسید، ۳ بایت به‌ازای هر نویسه صرفه‌جویی می‌شود؛
  • سرآیند HTTP، پارامترهای URL، جاسازی داخل رشته‌ای دیگر: از escape استفاده کنید تا از مشکلات رمزگذاری دور بمانید.

تنها نویسه‌هایی که باید escape شوند نویسه‌های کنترلی (U+0000 تا U+001F) هستند. مثلاً خط جدید باید \n نوشته شود؛ نمی‌توانید خط جدید واقعی داخل رشته بگذارید.

آیا ایموجی می‌تواند مستقیم در رشته برود؟

بله، اما یادتان باشد دو واحد کد UTF-16 مصرف می‌کند.

ایموجی و بسیاری نویسه‌های نادر (مانند 𠮷) بیرون از BMP هستند، پس در جاوااسکریپت "😀".length === 2. اعتبارسنجی بر اساس تعداد نویسه از این رو آسان اشتباه می‌شود:

const text = "😀";
console.log(text.length);          // 2, number of UTF-16 code units
console.log([...text].length);     // 1, number of code points
console.log(Buffer.byteLength(text)); // 4, number of UTF-8 bytes

اگر محدودیت طول نام مستعار با .length محاسبه شود، یک ایموجی دو جایگاه مصرف می‌کند؛ اگر بک‌اند بایت بشمارد، چهار. هنگام هم‌ترازی تعریف‌ها، روشن بگویید منظور نقطه کد، واحد کد یا بایت است.

۳. عددها و مقادیر ویژه

چرا عدد صحیح بزرگ 9007199254740993 به 9007199254740992 تبدیل شد؟

چون JSON.parse جاوااسکریپت عددها را به شناور دقت دوگانه IEEE 754 تبدیل می‌کند و بزرگ‌ترین عدد صحیح امن 2^53-1 است.

Number.MAX_SAFE_INTEGER === 9007199254740991  // true
9007199254740993                              // actually yields 9007199254740992

هر عدد بالاتر از 2^53-1 ممکن است در رقم‌های انتهایی‌اش گرد شود. قربانیان معمول: شناسه‌های کاربری ۱۹ رقمی تولیدشده با الگوریتم snowflake، شماره سفارش و مهرهای زمانی نانوثانیه‌ای.

سه راه‌حل، به ترتیب ترجیح:

  1. شناسه را در سرور به‌صورت رشته سریالایز کنید: "id": "9007199254740993"، مشکل دقت را از ریشه حذف می‌کند؛
  2. از کتابخانه تجزیه بدون‌اتلاف استفاده کنید: جایگزین‌های JSON.parse می‌توانند عدد صحیح بزرگ را عیناً حفظ کنند، مثلاً json-bigint؛
  3. برای نمایش فقط با عبارت باقاعده استخراج کنید: کل سند را تجزیه نکنید، فقط فیلد هدف را به‌صورت متن تطبیق دهید.

توجه کنید JSON.stringify مشکل متقارن دارد: عددی بیرون از بازه صحیح امن ممکن است پس از سریالایز شدن دیگر مقدار اصلی را نگه ندارد.

آیا NaN و Infinity می‌توانند وارد JSON شوند؟

نه. تولید عدد JSON فقط نشانه‌گذاری اعشاری را مجاز می‌داند و NaN، Infinity و -Infinity مقدار JSON معتبر نیستند.

رویکرد جاوااسکریپت «تخریب بی‌صدا» است:

JSON.stringify({ value: NaN });        // '{"value":null}'
JSON.stringify({ value: Infinity });   // '{"value":null}'

بدون خطا، NaN صرفاً به null تبدیل می‌شود. این صحنه کلاسیک «فرانت‌اند NaN فرستاد و بک‌اند null دریافت کرد» است. وقتی باید چنین معنایی را منتقل کنید، آن را صریح با رشته یا مقادیر نگهبان توافق‌شده بیان کنید:

{ "ratio": null, "ratioState": "not-a-number" }

آیا عددها می‌توانند به‌صورت 1e3 یا 0.5 نوشته شوند؟

بله. عددهای JSON اعشار و توان را پشتیبانی می‌کنند، پس 1e3، 1E3، 0.5 و -0.5 همه معتبرند.

اما محدودیت‌هایی وجود دارد:

  • نمی‌توانند با . شروع یا تمام شوند: .5 و 5. هر دو نامعتبرند، پس 0.5 و 5 بنویسید؛
  • بدون صفر پیشرو: 01 نامعتبر است؛
  • بدون هگزادسیمال: 0x1F نامعتبر است، آن افزونه JSON5 است؛
  • -0 معتبر است، اما JSON.stringify(-0) خروجی 0 می‌دهد و علامت را از دست می‌دهد.

۴. معناشناسی شیء

با کلیدهای تکراری چه اتفاقی می‌افتد؟

رفتار به پیاده‌سازی وابسته است؛ RFC 8259 فقط می‌گوید نام کلیدها «باید» یکتا باشند و چیزی درباره رفتار تکراری مشخص نمی‌کند.

در عمل اکثریت قریب‌به‌اتفاق پیاده‌سازی‌ها مقدار آخر را می‌گیرند:

JSON.parse('{"a":1,"a":2}')  // { a: 2 }

اما نمی‌توانید روی آن حساب کنید. برخی کتابخانه‌های Java، encoding/json در Go و json در Python بین نسخه‌ها متفاوت‌اند: برخی مقدار اول را می‌گیرند، برخی خطا می‌دهند و برخی آخر را. برخورد با کلید تکراری به‌عنوان خطا امن‌ترین رویکرد بین زبان‌هاست.

کلید تکراری را با چشم نمی‌توانید پیدا کنید — در JSON فشرده ممکن است همان کلید هزاران نویسه فاصله داشته باشد. از اسکریپت استفاده کنید:

# Extract keys that appear more than once at the same level, as a rough check
jq -r 'paths(scalars) | join(".")' data.json | sort | uniq -d

undefined به چه تبدیل می‌شود؟

بستگی دارد، و این باگ‌خیزترین نقطه است:

JSON.stringify({ a: undefined });              // '{}'  the key is dropped entirely
JSON.stringify({ a: undefined, b: 1 });        // '{"b":1}'
JSON.stringify([undefined]);                   // '[null]'  becomes null inside arrays
JSON.stringify({ a: function () {} });         // '{}'  functions are dropped too

همان undefined یعنی «فیلد ناپدید می‌شود» در شیء و «به null تبدیل می‌شود» در آرایه. اگر بک‌اند برای «فیلد گم‌شده» و «فیلد null است» شاخه‌های متفاوت بگیرد، اینجا ناسازگاری پیدا می‌کنید.

وقتی باید «بدون مقدار» را صریح بیان کنید، از null استفاده کنید نه undefined.

پس از تجزیه {"__proto__": {...}} چه اتفاقی می‌افتد؟

خود JSON.parse امن است: __proto__ به‌عنوان یک ویژگی معمولی خودی برخورد می‌شود و prototype را تغییر نمی‌دهد.

خطرناک عملیات ادغام پس از تجزیه است:

const patch = JSON.parse('{"__proto__": {"isAdmin": true}}');
Object.assign({}, patch);       // some implementations pollute the prototype

رویکرد درست این است که روی ویژگی‌های خودی پیمایش کنید و __proto__، constructor و prototype را رد کنید. جزئیات در پرسش‌های متداول عملکرد و امنیت است.

۵. مرجع سریع

نحو معتبر یادداشت
{"a": 1,} نه ویرگول انتهایی
{'a': 1} نه تک‌گیومه
{a: 1} نه کلید بدون گیومه
{"a": 1} // comment نه کامنت
"hello" بله رشته سطح بالا معتبر است، اما در API کم استفاده می‌شود
{"a": 1e3} بله توان پشتیبانی می‌شود
{"a": 01} نه صفر پیشرو
{"a": NaN} نه literal نامعتبر؛ stringify آن را به null تبدیل می‌کند
\uFEFF{"a":1} نه BOM بخشی از متن JSON نیست
{"a":1,"a":2} محل اختلاف پیاده‌سازی‌ها مقدار آخر را می‌گیرند، اما به آن تکیه نکنید