JSON Toolkit

הגבולות של המרת JSON ↔ YAML ו-XML

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

בינוניכ-12 דקות קריאהפורסם 2026-10-30עודכן 2026-10-30
תמונה עבור הגבולות של המרת JSON ↔ YAML ו-XML

שורה תחתונה: השתמשו ב-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 חמישה אובדנים בלתי נמנעים

  1. סדר האלמנטים: אובייקטי JSON אינם מסודרים, בעוד ש-XML הוא רצף מסודר של אלמנטים;
  2. מאפיינים מול אלמנטים בני: הם נושאים סמנטיקה שונה ב-XML, אבל שניהם הופכים לשדות רגילים ב-JSON;
  3. תוכן מעורב: מבנים כמו <p>你好 <b>世界</b> 再见</p> אינם ניתנים לביטוי באובייקט רגיל;
  4. הצהרות מרחב שמות: xmlns הוא מושג של תיחום; ברגע שהוא נשטח לשדות, התיחום נעלם;
  5. CDATA וישויות: <![CDATA[...]]> ו-&amp; הם דבר לא יותר מטקסט ב-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 ומרחבי שמות כרוכים באובדן בין הפורמטים, וסנכרון דו-כיווני ממשיך לצבור הבדלים. בחרו פורמט מקור אחד והתייחסו לאחרים כייצוא חד-פעמי או כתצוגה זמנית.