הגבולות של המרת JSON ↔ YAML ו-XML
שורה תחתונה: הטענה ש-JSON הוא תת-קבוצה של YAML מבטיחה רק כיוון אחד, ול-XML אין מיפוי JSON רשמי כלל — הכול נשען על מוסכמה.

שורה תחתונה: השתמשו ב-JSON כפורמט הביניים: פענחו את המקור למודל נתונים, ואז סריאליזו ליעד; לפני ההמרה, החליטו אם הערות, מאפייני XML ומרחבי שמות, וטיפוסים מספריים או בוליאניים חייבים לשרוד.
השאלה אם צינור המרה שמיש אינה תלויה בכמה יפה נראה הפלט, אלא במידת המודעות שלכם לכמה הוא זרק.
1. JSON ו-YAML: הכלה חד-כיוונית
מפרט YAML 1.2 אומר זאת בבירור: JSON הוא תת-קבוצה של YAML. כלומר, כל JSON תקין הוא YAML תקין:
{"name": "json", "tags": ["a", "b"], "count": 3}
הקטע הזה הוא גם JSON וגם YAML בסגנון flow. ההיפך אינו נכון.
| תכונה | JSON | YAML 1.2 |
|---|---|---|
| הערות | ✗ | ✓ |
| עוגנים וכינויים | ✗ | ✓ |
מסמכים מרובים (---) |
✗ | ✓ |
| מפתחות שאינם מחרוזות | ✗ | ✓ (1: a, true: b) |
| מפתחות מורכבים | ✗ | ✓ (? [a, b]) |
| רגישות להזחה | ✗ | ✓ (הזחת טאב נדחית) |
| הסקת טיפוסים | מפורשת | משתמעת (0o17, 0x1F, .inf) |
| קריאות בשורה אחת | טובה | ירודה (תוכן ארוך נשען על תחביר קיפול) |
1.1 הסקת טיפוסים היא המלכודת הגדולה ביותר
YAML מפענח כמה ערכים שנראים כמחרוזות לטיפוסים אחרים:
zip: 01234 # מחרוזת או שלם אוקטלי? סכימת הליבה של YAML 1.2 מתייחסת אליו כמחרוזת
version: 1.10 # 1.1 (float); הסמנטיקה אובדת
country: NO # מחרוזת ב-YAML 1.2; בוליאני false ב-YAML 1.1
date: 2026-10-30 # מימושים מסוימים מפענחים אותו כאובייקט תאריך
הדוגמה של NO צרבה הרבה אנשים: עם מפענח YAML 1.1 (כמה ספריות ותיקות), country: NO הופך ל-false.
אמצעי נגד: עטפו בגרשיים כל מזהה ש"נראה כמו מספר או בוליאני", כמו version: "1.10".
1.2 עוגנים, כינויים ומסמכים מרובים
defaults: &defaults
retries: 3
timeout: 30
serviceA:
<<: *defaults
host: a.example.com
---
serviceB:
<<: *defaults
host: b.example.com
אחרי הרחבת העוגנים והמרה ל-JSON תגיעו בדרך כלל לשני אובייקטי תצורה מוכפלים, או למערך אחד (מסמכים מרובים חייבים להפוך ל-[{...}, {...}]).
כך או כך, העוגנים אינם משוחזרים בהמרה ההפוכה — האובדן הזה בלתי נמנע.
1.3 המרה שמשמרת הערות
רוב הספריות פשוט זורקות הערות בזמן ההמרה. כשצריך לשמור אותן, השתמשו ב-Document API של חבילת yaml:
import { parseDocument } from 'yaml';
const document = parseDocument(`
# תצורת שירות
port: 8080
`);
document.set('port', 3000);
console.log(document.toString());
// # תצורת שירות
// port: 3000
הבדיקה פשוטה: אם הקובץ הזה ייערך על ידי בני אדם שוב ושוב, אל תיקחו את המסלול "פענוח → אובייקט → סריאליזציה".
1.4 איך מאמתים עקביות של סבב הלוך ושוב
/**
* מאמת עקביות של סבב הלוך ושוב עבור JSON → YAML → JSON.
* @param sourceValue ערך ה-JSON המקורי
* @param stringify פונקציית הסריאליזציה ל-YAML
* @param parse פונקציית הפענוח של YAML
* @returns האם שתי התוצאות שוות בדיוק
*/
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, ומחרוזות ארוכות המכילות שורות חדשות.
2. JSON ו-XML: אין מיפוי תקני
XML קדם ל-JSON בהרבה ומחזיק ביכולות מבניות חזקות הרבה יותר (מאפיינים, מרחבי שמות, תוכן מעורב, DTD). בין השניים אין מפרט מיפוי רשמי; כל כלי עושה את אותו דבר: בוחר מוסכמה, ואז מנסה לא לאבד הרבה.
2.1 מוסכמות נפוצות
| מוסכמה | ייצוג | דוגמה |
|---|---|---|
| אלמנט → מפתח אובייקט | אלמנט בעל אותו שם הופך לשדה | <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": "钢笔" }
]
}
}
2.2 חמישה אובדנים בלתי נמנעים
- סדר האלמנטים: אובייקטי JSON אינם מסודרים, בעוד ש-XML הוא רצף מסודר של אלמנטים;
- מאפיינים מול אלמנטים בני: הם נושאים סמנטיקה שונה ב-XML, אבל שניהם הופכים לשדות רגילים ב-JSON;
- תוכן מעורב: מבנים כמו
<p>你好 <b>世界</b> 再见</p>אינם ניתנים לביטוי באובייקט רגיל; - הצהרות מרחב שמות:
xmlnsהוא מושג של תיחום; ברגע שהוא נשטח לשדות, התיחום נעלם; - CDATA וישויות:
<![CDATA[...]]>ו-&הם דבר לא יותר מטקסט ב-JSON.
2.3 הדילמה של "אלמנט אחד או מערך"
<list><item>1</item></list>
עם item בודד, xml2js מחזיר אובייקט כברירת מחדל ויחזיר מערך רק כשיש שניים.
כתוצאה מכך, קוד הצד הלקוח חייב לכתוב Array.isArray(x) ? x : [x].
אמצעי נגד: הפעילו את האפשרות "always array" בזמן הפענוח, או הצהירו על השדה כטיפוס מערך לפי schema.
import { parseStringPromise } from 'xml2js';
const parsed = await parseStringPromise(xmlText, {
explicitArray: true, // החזירו מערך גם עבור אלמנט בודד
trim: true,
explicitRoot: false,
});
2.4 תזכורת אבטחה
לפני פענוח XML לא מהימן חייבים להשבית ישויות חיצוניות, אחרת אפשר להפעיל XXE (קריאת קבצים מקומיים, גישוש אחר רשתות פנימיות):
// באקוסיסטם של Node, DTD-ים וישויות חיצוניות מושבתים בדרך כלל באמצעות אפשרויות המפענח
const parserOptions = { doctype: false, externalEntities: false };
XML → JSON הוא רק השכבה הראשונה של ההמרה; ההמרה אינה מבטלת את משטח התקיפה ש-XML מביא.
3. היכן כל אחד משלושת הפורמטים מתאים
| תרחיש | פורמט מומלץ | הסיבה |
|---|---|---|
| APIs ציבוריים ב-HTTP | JSON | נתמך מקורית בדפדפנים ובכל שפה |
| תצורה שמתוחזקת ביד | YAML | הערות, הזחה, קריאות |
| מסמכים שזקוקים לסכימה חזקה ומרחבי שמות (SOAP, Office Open XML, SVG) | XML | יכולות מבניות והאקוסיסטם הקיים |
| אינטראקציה עם כלי תפעול (K8s, CI) | YAML | ברירת המחדל של האקוסיסטם |
| התמדה, מטמונים, לוגים | JSON / JSONL | אוניברסלי ובלי תלות |
4. הערות מימוש
import { stringify as toYaml } from 'yaml';
/**
* ממיר ערך JSON לטקסט YAML תוך שמירת תווים סיניים ללא escape.
* @param value ערך ה-JSON להמרה
* @returns טקסט ה-YAML
*/
function jsonToYaml(value: unknown): string {
return toYaml(value, {
indent: 2,
lineWidth: 0, // השביתו גלישה אוטומטית כדי שמחרוזות ארוכות לא ייפלטו
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,
};
5. רשימת בדיקה
- [ ] כל מזהה "שנראה כמו מספר/בוליאני" ב-YAML עטוף בגרשיים;
- [ ] קבוצת דוגמאות אימות עבור JSON → YAML → JSON מוגדרת ומכוסה בבדיקות;
- [ ] כשהערות חייבות להישמר, משתמשים ב-Document API במקום בסבב אובייקט;
- [ ] מוסכמת המיפוי של XML כתובה והצוות חולק סט פרמטרים אחד;
- [ ] DTD-ים וישויות חיצוניות מושבתים בזמן פענוח XML לא מהימן;
- [ ] מספרי בתים ומספרי שדות מושווים לפני ואחרי ההמרה, וכל חריגה מעלה התראה.
שאלות נפוצות
איך בוחרים את כיוון ההמרה בין JSON, YAML ו-XML?
העדיפו את JSON כפורמט הביניים: פענחו קודם YAML ו-XML למודל נתונים, ואז סריאליזו מהמודל במקום לשכתב טקסט. השתמשו ב-Document API של YAML כשהערות חייבות לשרוד, וקבעו מוסכמת מיפוי XML אחת לפני שצריך מאפיינים או מרחבי שמות.
למה המרת XML ל-JSON מאבדת מאפיינים ומרחבי שמות?
ל-JSON אין מושג של מאפיינים או מרחבי שמות, ולכן מאפייני XML, תחיליות ו-URI-ים של מרחבי שמות חייבים להיות מקופלים למפתחות רגילים, בדרך כלל עם תחילית @ או צומת #text. ברגע שכללי המיפוי מתפצלים, אותו XML מפיק JSON שונה ואינו ניתן לסבב הלוך ושוב.
כדאי לשמור JSON, YAML ו-XML בסנכרון דו-כיווני לטווח ארוך?
לא. הערות, עוגנים, מאפייני XML ומרחבי שמות כרוכים באובדן בין הפורמטים, וסנכרון דו-כיווני ממשיך לצבור הבדלים. בחרו פורמט מקור אחד והתייחסו לאחרים כייצוא חד-פעמי או כתצוגה זמנית.