مرزهای تبدیل JSON ↔ YAML و XML
جمعبندی: ادعای اینکه JSON زیرمجموعه YAML است فقط یک جهت را تضمین میکند و XML اصلاً نگاشت رسمی JSON ندارد — همه چیز بر قرارداد استوار است.

جمعبندی: از JSON بهعنوان قالب واسط استفاده کنید: منبع را به یک مدل داده تجزیه کنید و سپس به هدف سریال کنید؛ پیش از تبدیل تصمیم بگیرید آیا کامنتها، صفتها و فضاهای نام XML و نوعهای عددی یا بولین باید زنده بمانند.
اینکه یک خط لوله تبدیل قابل استفاده است یا نه، به زیبایی خروجی بستگی ندارد، بلکه به آگاهی شما از مقدار چیزی که دور ریخته است.
۱. JSON و YAML: شمول یکطرفه
مشخصات YAML 1.2 صریح میگوید: JSON زیرمجموعه YAML است. یعنی هر JSON معتبر، YAML معتبر نیز هست:
{"name": "json", "tags": ["a", "b"], "count": 3}
این قطعه هم JSON است و هم سبک flow در YAML. عکس آن برقرار نیست.
| ویژگی | JSON | YAML 1.2 |
|---|---|---|
| کامنت | ✗ | ✓ |
| لنگر و نام مستعار | ✗ | ✓ |
چند سند (---) |
✗ | ✓ |
| کلید غیررشتهای | ✗ | ✓ (1: a، true: b) |
| کلید پیچیده | ✗ | ✓ (? [a, b]) |
| حساس به تورفتگی | ✗ | ✓ (تورفتگی با تب رد میشود) |
| استنتاج نوع | صریح | ضمنی (0o17، 0x1F، .inf) |
| خوانایی تکخطی | خوب | ضعیف (محتوای بلند به نحو folding تکیه دارد) |
۱.۱ استنتاج نوع بزرگترین تله است
YAML برخی مقدارها را که شبیه رشتهاند به نوعهای دیگر تجزیه میکند:
zip: 01234 # string or octal integer? the YAML 1.2 core schema treats it as a string
version: 1.10 # 1.1 (a float); the semantics are lost
country: NO # a string in YAML 1.2; boolean false in YAML 1.1
date: 2026-10-30 # some implementations parse it as a date object
مثال NO بسیاری را سوزانده است: با یک تجزیهکننده YAML 1.1 (برخی کتابخانههای قدیمیتر)، country: NO به false تبدیل میشود.
اقدام متقابل: هر شناسهای که «شبیه عدد یا بولین به نظر میرسد» را در گیومه بگذارید، مانند version: "1.10".
۱.۲ لنگرها، نامهای مستعار و چند سند
defaults: &defaults
retries: 3
timeout: 30
serviceA:
<<: *defaults
host: a.example.com
---
serviceB:
<<: *defaults
host: b.example.com
پس از گسترش لنگرها و تبدیل به JSON معمولاً به دو object پیکربندی تکراری میرسید یا به یک آرایه (چند سند باید به [{...}, {...}] تبدیل شوند).
در هر صورت، لنگرها در تبدیل معکوس بازگردانی نمیشوند — این تلفات اجتنابناپذیر است.
۱.۳ تبدیل با حفظ کامنت
بیشتر کتابخانهها در جریان تبدیل بهسادگی کامنتها را میریزند. وقتی باید آنها را نگه دارید، از Document API بسته yaml استفاده کنید:
import { parseDocument } from 'yaml';
const document = parseDocument(`
# Service configuration
port: 8080
`);
document.set('port', 3000);
console.log(document.toString());
// # Service configuration
// port: 3000
آزمون ساده است: اگر این فایل بارها و بارها توسط انسان ویرایش میشود، مسیر «تجزیه → object → سریالسازی» را در پیش نگیرید.
۱.۴ سازگاری رفتوبرگشت را چگونه بررسی کنیم
/**
* Verifies round-trip consistency for JSON → YAML → JSON.
* @param sourceValue Original JSON value
* @param stringify YAML serialization function
* @param parse YAML parsing function
* @returns Whether the two results are exactly equal
*/
function isRoundTripStable(
sourceValue: unknown,
stringify: (input: unknown) => string,
parse: (input: string) => unknown,
): boolean {
const yamlText = stringify(sourceValue);
const restored = parse(yamlText);
return JSON.stringify(restored) === JSON.stringify(sourceValue);
}
اجرای آن روی داده واقعی ارزشمندتر از ده بار خواندن مستندات است. مطمئن شوید مجموعه نمونه شما پوشش میدهد: null، رشته خالی، آرایه خالی، "00"، "1e5"، صفر منفی، عددهای صحیح بزرگتر از 2^53 و رشتههای بلند دارای خط جدید.
۲. JSON و XML: نگاشت استانداردی وجود ندارد
XML بسیار پیشتر از JSON آمده و توانایی ساختاری بسیار قویتری دارد (صفتها، فضاهای نام، محتوای مختلط، DTD). میان این دو هیچ مشخصات نگاشت رسمی وجود ندارد؛ هر ابزاری همان کار را میکند: یک قرارداد انتخاب میکند و سپس میکوشد چیز زیادی از دست ندهد.
۲.۱ قراردادهای رایج
| قرارداد | نمایش | مثال |
|---|---|---|
| عنصر → کلید object | یک عنصر همنام به یک فیلد تبدیل میشود | <a>1</a> → {"a": "1"} |
صفت → پیشوند @ |
@name |
<a id="1"/> → {"a": {"@id": "1"}} |
متن → #text |
وقتی هم صفت و هم متن هست | <a id="1">hi</a> → {"a": {"@id": "1", "#text": "hi"}} |
| عنصر تکراری → آرایه | دو یا چند تکرار به آرایه تبدیل میشود | <i>1</i><i>2</i> → {"i": ["1","2"]} |
| فضای نام → پیشوند | ns:tag |
<ns:tag/> → {"ns:tag": ...} |
<order id="A1001">
<item sku="X1" qty="2">笔记本</item>
<item sku="X2" qty="1">钢笔</item>
</order>
{
"order": {
"@id": "A1001",
"item": [
{ "@sku": "X1", "@qty": "2", "#text": "笔记本" },
{ "@sku": "X2", "@qty": "1", "#text": "钢笔" }
]
}
}
۲.۲ پنج تلفات اجتنابناپذیر
۱. ترتیب عناصر: objectهای JSON بیترتیباند، حال آنکه XML دنبالهای مرتب از عناصر است؛
۲. صفت در برابر عنصر فرزند: در XML معنای متفاوتی دارند، اما هر دو در JSON به فیلد ساده تبدیل میشوند؛
۳. محتوای مختلط: ساختارهایی مانند <p>你好 <b>世界</b> 再见</p> را نمیتوان با object ساده بیان کرد؛
۴. اعلان فضای نام: xmlns مفهومی مربوط به دامنه است؛ وقتی به فیلد تخت شد، دامنه از میان میرود؛
۵. CDATA و موجودیتها: <![CDATA[...]]> و & در JSON چیزی بیش از متن نیستند.
۲.۳ دوراهی «یک عنصر یا یک آرایه»
<list><item>1</item></list>
با یک item، xml2js بهطور پیشفرض object برمیگرداند و تنها وقتی دو تا شد آرایه میدهد.
در نتیجه کد فرانتاند مجبور است Array.isArray(x) ? x : [x] بنویسد.
اقدام متقابل: هنگام تجزیه گزینه «همیشه آرایه» را روشن کنید یا فیلد را بر اساس schema از نوع آرایه اعلام کنید.
import { parseStringPromise } from 'xml2js';
const parsed = await parseStringPromise(xmlText, {
explicitArray: true, // return an array even for a single element
trim: true,
explicitRoot: false,
});
۲.۴ یادآوری امنیتی
پیش از تجزیه XML غیرقابلاعتماد باید موجودیتهای خارجی را غیرفعال کنید، وگرنه ممکن است XXE را فعال کنید (خواندن فایلهای محلی، کاوش شبکه داخلی):
// In the Node ecosystem, DTDs and external entities are usually disabled through parser options
const parserOptions = { doctype: false, externalEntities: false };
XML → JSON فقط لایه نخست تبدیل است؛ تبدیل سطح حملهای که XML میآورد را از میان نمیبرد.
۳. هر یک از سه قالب کجا مناسب است
| سناریو | قالب پیشنهادی | دلیل |
|---|---|---|
| APIهای عمومی HTTP | JSON | پشتیبانی بومی مرورگرها و همه زبانها |
| پیکربندی دستنگهدار | YAML | کامنت، تورفتگی، خوانایی |
| اسنادی که به schema قوی و فضای نام نیاز دارند (SOAP، Office Open XML، SVG) | XML | توانایی ساختاری و اکوسیستم موجود |
| تعامل با ابزارهای عملیاتی (K8s، CI) | YAML | پیشفرض اکوسیستم |
| ماندگاری، کش، گزارش | JSON / JSONL | همهگیر و بدون نیاز به وابستگی |
۴. نکات پیادهسازی
import { stringify as toYaml } from 'yaml';
/**
* Converts a JSON value to YAML text while keeping Chinese characters unescaped.
* @param value The JSON value to convert
* @returns The YAML text
*/
function jsonToYaml(value: unknown): string {
return toYaml(value, {
indent: 2,
lineWidth: 0, // disable automatic wrapping so long strings are not split
defaultStringType: 'QUOTE_DOUBLE',
});
}
چند گزینه کاربردی:
lineWidth: 0شکستن خودکار خط را خاموش میکند؛ وگرنه URLهای بلند در چند خط میشکنند و خواندنشان دشوار میشود؛- گذاشتن همه چیز در گیومه دوتایی جلوی بدخوانی مقدارهایی مانند
NOو1.10را میگیرد؛ - برای دادهای با دقت عددی حساس، پیش از سریالسازی به رشته تبدیل کنید.
برای جهت XML، یک تابع نگاشت واحد تثبیت کنید و «پیشوند صفت»، «نام کلید متن» و «استراتژی آرایه» را در امضای آن بگذارید نه پراکنده در اطراف:
type XmlMappingOptions = {
attributePrefix: string;
textKey: string;
alwaysArray: boolean;
};
const defaultMapping: XmlMappingOptions = {
attributePrefix: '@',
textKey: '#text',
alwaysArray: false,
};
۵. چکلیست
- [ ] هر شناسه «شبیه عدد/بولین» در YAML در گیومه گذاشته شده است؛
- [ ] مجموعه نمونه اعتبارسنجی برای JSON → YAML → JSON تعریف و در آزمونها پوشش داده شده است؛
- [ ] وقتی کامنتها باید حفظ شوند، از Document API استفاده میشود نه رفتوبرگشت object؛
- [ ] قرارداد نگاشت XML مکتوب شده و تیم یک مجموعه پارامتر مشترک دارد؛
- [ ] هنگام تجزیه XML غیرقابلاعتماد، DTDها و موجودیتهای خارجی غیرفعالاند؛
- [ ] تعداد بایتها و فیلدها پیش و پس از تبدیل مقایسه میشود و هر ناهنجاری هشدار میدهد.
پرسشهای متداول
چگونه جهت تبدیل میان JSON، YAML و XML را انتخاب کنم؟
JSON را بهعنوان قالب واسط ترجیح دهید: نخست YAML و XML را به یک مدل داده تجزیه کنید و سپس از همان مدل سریال بگیرید، نه اینکه متن را بازنویسی کنید. وقتی کامنتها باید زنده بمانند از Document API بسته YAML استفاده کنید و پیش از آنکه به صفتها یا فضاهای نام نیاز پیدا کنید، یک قرارداد نگاشت XML را تثبیت کنید.
چرا تبدیل XML به JSON صفتها و فضاهای نام را از دست میدهد؟
JSON مفهومی به نام صفت یا فضای نام ندارد، بنابراین صفتها، پیشوندها و URIهای فضای نام XML باید در کلیدهای معمولی تاشوند، معمولاً با پیشوند @ یا گره #text. وقتی قواعد نگاشت واگرا شوند، همان XML، JSON متفاوتی تولید میکند و رفتوبرگشت ممکن نیست.
آیا باید JSON، YAML و XML را در همگامسازی دوسویه بلندمدت نگه دارم؟
نه. کامنتها، لنگرها، صفتها و فضاهای نام XML در قالبهای مختلف تلف میشوند و همگامسازی دوسویه مدام تفاوتها را انباشته میکند. یک قالب منبع انتخاب کنید و بقیه را خروجی یکبارمصرف یا نمای موقت بدانید.