ממיר JSON ל-YAML

ממיר JSON ל-YAML ומסביר כל זוג מרכאות שהוא מוסיף — המחרוזות שהיו הופכות בשקט לבוליאני, למספר או לתאריך בלעדיהן.

קלט
YAML
name: deploy
country: 'NO'
startsAt: '12:30'
mode: '0755'
version: '1.10'
released: '2024-01-30'
enabled: 'yes'
script: |-
  set -e
  npm run build
  npm test
replicas: 3
tags:
  - web
  - edge
למה כל מחרוזת מצוטטת

ערכים מצוטטים: 6. נדרשים רק ב-YAML 1.1: 4. החליפו את הסכמה למעלה כדי לראות את ההבדל.

  • countryרק 1.1

    «NO» היה נקרא כבוליאני false.

  • startsAtרק 1.1

    «12:30» היה נקרא בבסיס 60 כ-750.

  • mode

    ל-«0755» יש אפס מוביל, ולכן הוא היה נקרא כמספר 493.

  • version

    «1.10» היה נקרא כמספר 1.1.

  • releasedרק 1.1

    «2024-01-30» היה נקרא כתאריך ולא כטקסט.

  • enabledרק 1.1

    «yes» היה נקרא כבוליאני true.

מה הכלי הזה עושה

הוא ממיר מסמך JSON ל-YAML, ואז אומר לכם למה כל מחרוזת יצאה במרכאות. החלק השני הוא הסיבה שהכלי קיים: כל ממיר אחר מחזיר את הפלט ומשאיר אתכם לגלות מאוחר יותר שאחד הערכים שלכם כבר אינו מחרוזת.

ההמרה רצה בכיוון אחד. קריאת YAML היא בעיה גדולה בהרבה מכתיבתו, ומנתח YAML נכון-בחלקו גרוע מכלום — הוא מקבל את הקובץ שלכם ומחזיר את הנתונים השגויים בלי להתלונן. פליטה היא בעיה תחומה, וזה הכיוון שמכוסה כאן.

למה ממיר בכלל צריך דעות

‏JSON אומר מה הטיפוס של כל דבר. למחרוזת יש מרכאות, למספר אין, ואין אפשרות שלישית. ‏YAML לעומת זאת קובע את הטיפוס של ערך לא מצוטט לפי הצורה שלו: אם הוא תואם את התבנית של בוליאני הוא בוליאני, אם הוא תואם מספר הוא מספר, ורק אם אינו תואם לשום דבר הוא נשאר טקסט.

זה מה שהופך את YAML לנעים לכתיבה ביד, וזה מה שהופך המרה אליו לשאלה של שיקול דעת. כל מחרוזת בקלט צריכה להיבדק מול כל תבנית ש-YAML פותר, ולהיות מצוטטת אם היא תואמת לאחת. תטעו לכיוון הבטוח והפלט רועש; תטעו לכיוון השני וערך משנה טיפוס בשקט.

בעיית נורווגיה, וקרובי משפחתה

המקרה המפורסם ביותר הוא רשימת קודי מדינות. נורווגיה היא NO, וב-YAML 1.1 האסימון הלא מצוטט NO הוא הבוליאני false. קובץ תצורה שמונה מדינות מאבד את נורווגיה ומקבל false, ושום דבר בשום מקום אינו מדווח על שגיאה.

זה אינו כלל מוזר אחד אלא משפחה שלמה. ‏YAML 1.1 קורא את כל y, ‏Y, ‏yes, ‏no, ‏on ו-off כבוליאנים, בכל צורת רישיות, וזה תופס סמל כימי, מצב של מתג ותשובה לשאלה. ופותרי המספרים מוזרים אף יותר:

  • ‏12:30 הוא 750. ‏YAML 1.1 קורא ספרות מופרדות בנקודתיים בבסיס 60, ולכן שעה ביום או משך זמן הופכים לשלם.
  • ‏0755 הוא 493. אפס מוביל פירושו אוקטלי ב-YAML 1.1 — ובגרסה 1.2 אותו טקסט הוא 755 העשרוני, כך ששתי הגרסאות חלוקות על איזה מספר ולא על האם.
  • ‏1.10 הוא 1.1. מספר גרסה דו-חלקי הוא נקודה צפה, והאפס הסופי נעלם. תלות שהוצמדה ל-1.10 מצביעה עכשיו על 1.1.
  • ‏2024-01-30 הוא אובייקט תאריך ולא מחרוזת, מפני של-YAML 1.1 יש טיפוס חותמת זמן.
  • מחרוזת ריקה היא null, וכך גם המילים העירומות null, ‏Null, ‏NULL והטילדה.

אף אחד מאלה אינו באג ב-YAML. אלה הפותרים עושים בדיוק את מה שהם מבטיחים, על טקסט שבמקרה תואם. ההגנה היחידה היא לצטט כל דבר שתואם, וזה מה שהכלי הזה עושה ומה שלוח הממצאים מפרט שורה אחר שורה.

שתי גרסאות, ולמה הישנה היא ברירת המחדל

‏YAML 1.2 הגיע ב-2009 והסיר את רוב הפותרים המפתיעים. סכמת הליבה שלו שומרת רק את true ואת false כבוליאנים, מוותרת על בסיס 60 לגמרי, ואין לה טיפוס חותמת זמן. תחת 1.2, ‏NO ו-12:30 ו-2024-01-30 כולם פשוט מחרוזות.

המלכוד הוא מה באמת קורא את הקובץ שלכם. ‏PyYAML מממש את YAML 1.1, ו-PyYAML הוא המנתח שמאחורי כמות עצומה של כלים — Ansible, לקוחות Kubernetes ישנים, אינספור סקריפטים. ‏yaml.v3 של Go ו-js-yaml הנוכחי הולכים אחרי 1.2. כך שאותו מסמך עצמו יכול להיקרא בשתי דרכים שונות תלוי במי שפותח אותו, והפלט היחיד שבטוח בכל מקום הוא כזה שמצוטט עבור 1.1.

זו ברירת המחדל כאן. החלפת ההגדרה ל-1.2 אינה מסתירה דבר — היא פולטת מחדש עם הפותרים החדשים ולוח הממצאים מתכווץ, כך שאפשר לראות במדויק אילו מרכאות היו שם בשביל הסכמה הישנה. אלה שנשארות הן אלה שכל מנתח זקוק להן.

מחרוזות ששוברות את התחביר ולא את הטיפוס

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

  • נקודתיים ואחריהן רווח מסיימות מפתח. ‏«note: time: now» היה נקרא כמפתח note שערכו מפתח time.
  • רווח ואחריו סולמית פותחים הערה, ולכן כל מה שאחריה נעלם.
  • ‏-, ‏?, ‏:, ‏[, ‏], ‏{, ‏}, ‏#, ‏&, ‏*, ‏!, ‏|, ‏>, ‏%, ‏@ או גרש הפוך בתחילת המחרוזת הם תווי סימון ופירושם מבני.
  • רווח מוביל או נגרר אינו נשמר בערך לא מצוטט, ולכן «‏ x ‏» חוזר כ-«x».
  • טאב בכל מקום בערך נדחה מכול וכול — ‏PyYAML מסרב לכל המסמך במקום לקרוא אותו שגוי, ולכן זה נכשל בקול.

מרכאות בודדות משמשות בכל מקום שהן מספיקות, מפני שיש להן בדיוק כלל מילוט אחד — גרש נכתב פעמיים — והן קריאות יותר ממילוטי הלוכסן שמרכאות כפולות מביאות. מרכאות כפולות שמורות למקרים שבאמת זקוקים למילוט: תווי בקרה, טאבים, ומחרוזות רב-שורתיות שאינן יכולות להשתמש בבלוק.

מחרוזות רב-שורתיות ומציין החיתוך

מחרוזת שמכילה שורות חדשות — סקריפט, תעודה, גוש פרוזה — היא בדרך כלל הסיבה שמישהו רוצה YAML מלכתחילה. ‏JSON יכול לכתוב אותה רק עם מילוטי לוכסן-n בשורה אחת ארוכה מאוד; ל-YAML יש את הבלוק הסקלרי הליטרלי, שנפתח בקו אנכי, שבו הטקסט מופיע מוזח וקריא מתחת.

הדקות היא מה קורה לשורות החדשות בסוף, ואת זה קובע מציין החיתוך:

  • קו אנכי עירום חותך: כמה שורות חדשות נגררות שיהיו לבלוק, הערך מקבל בדיוק אחת.
  • קו אנכי ואחריו מינוס מסיר: הערך אינו מקבל אף אחת.
  • קו אנכי ואחריו פלוס שומר: הערך מקבל את כולן.

הכלי הזה בוחר את המציין לפי המחרוזת שקיבל, כך שהערך שורד את המעבר בדיוק. כדאי לדעת את זה מפני שברירת המחדל — הקו האנכי העירום — היא מה שאנשים כותבים ביד, והיא מנרמלת בשקט מחרוזת שהסתיימה בשתי שורות חדשות או באפס.

בלוק סקלרי אינו יכול לשאת הכול, ובמקום שאינו יכול, הפלט נופל למרכאות כפולות ולוח הממצאים אומר למה. החזרת גררה אינה שורדת, מפני שבלוקים סקלריים מנרמלים שבירות שורה. שורה ראשונה שמתחילה ברווח הייתה נקראת כהזחה נוספת ומוסרת. ושורה שמסתיימת ברווח נשמרת לפי המפרט אבל בלתי נראית על המסך ומוסרת ברוב העורכים בשמירה, ולכן ציטוט הוא הבחירה הבטוחה יותר — זו החלטה ולא מגבלה, והיא מדווחת ככזו.

יותר ממסמך אחד

ל-YAML יש משהו של-JSON אין: קובץ יכול להחזיק זרם של מסמכים מופרדים בשלושה מקפים. זה הפורמט של מניפסט Kubernetes, וזו הסיבה שמערך JSON כל כך לעיתים קרובות צריך להפוך למשהו אחר מרצף YAML.

המתג כאן פולט כל איבר של מערך עליון כמסמך משלו. וכשהקלט אינו ערך JSON יחיד כלל אלא כל שורה מנתחת בפני עצמה, הוא נקרא כ-NDJSON — הפורמט מופרד-השורות שיומנים ויצואי API מגיעים בו — וכל שורה הופכת למסמך. הקריאה הזאת היא ניחוש, ולכן היא מדווחת מעל הפלט ולא נעשית בשקט.

הנפילה חלה רק כשכל הקלט נכשל בניתוח וכל שורה מצליחה, מה שמסמך פגום סתם לא יעשה. שגיאת תחביר עדיין עולה כשגיאת תחביר, עם השורה והעמודה שבהן אירעה.

מה שההמרה אינה יכולה לשמר

שני דברים אובדים לפני שהכלי הזה רואה את הנתונים שלכם, שניהם בניתוח ה-JSON עצמו, וכדאי לדעת אילו.

מפתחות כפולים. ‏JSON מרשה לאובייקט למנות את אותו מפתח פעמיים ורוב המנתחים שומרים את האחרון בשקט. ‏YAML אוסר כפילויות מכול וכול, ולכן הפלט יהיה תקין, אבל הערך המוקדם כבר איננו — ושום ממיר אינו יכול לדווח על מה שמעולם לא קיבל.

דיוק שלמים. מספר JSON גדול מכתשעה קוודריליון אינו שורד ניתוח לתוך double, ולכן מזהה כמו 12345678901234567890 חוזר מעוגל. זו אינה בעיה של YAML ואינה בעיה שהכלי הזה מכניס; היא קורית בכל מנתח JSON בשפה. אם מזהה גדול חשוב, מקומו במחרוזת בשני הצדדים.

הערות על הפלט

ההזחה היא רווחים, תמיד, מפני ש-YAML אוסר טאבים להזחה לחלוטין — זה אחד הדברים המעטים שהפורמט קפדן לגביהם. שני רווחים הם המוסכמה; ארבעה מוצעים מפני שיש סגנונות בית שרוצים אותם.

רצפים מוזחים מתחת למפתח שלהם. גם זה וגם הצורה הלא מוזחת הם YAML חוקי ומשמעם זהה; המוזחת היא מה שרוב האנשים כותבים ומה שרוב העורכים מקפלים נכון.

מערך או אובייקט ריקים נכתבים בסגנון זרימה כזוג סוגריים, מפני שלסגנון הבלוק אין דרך לבטא ריקנות — אין מה לכתוב בשורות שאחריו.

הפלט מסתיים בשורה חדשה, וזה נושא משקל ולא סדר. חיתוך של בלוק סקלרי נמדד מול שבירת השורה שאחריו, ולכן בלוק חתוך ממש בסוף קובץ בלי שורה חדשה סופית מאבד את השורה החדשה שהיה אמור לשמור.

שאלות נפוצות

האם JSON כבר YAML תקין?
תחת YAML 1.2, כן: המפרט אומר זאת במפורש, ומנתח 1.2 יקרא קובץ JSON ישירות. זה לא מועיל במיוחד בפועל, מפני שהסיבה להמיר היא קריאוּת — הערות, בלוקים סקלריים, בלי סוגריים מסולסלים — והדבקת JSON לתוך קובץ YAML לא נותנת לכם אף אחד מהם. תחת YAML 1.1 זה לא לגמרי נכון, וזו עוד סיבה שכדאי להבחין בין שתי הגרסאות.
למה המחרוזת שלי קיבלה מרכאות שנראה שאינה זקוקה להן?
כמעט בוודאות היא כן זקוקה. ‏YAML קובע טיפוס של ערך לא מצוטט לפי התאמת תבניות, ולכן NO, ‏yes, ‏off, ‏12:30, ‏0755, ‏1.10, ‏2024-01-30 ומחרוזת ריקה כולם חדלים להיות מחרוזות. לוח הממצאים נוקב בכל אחד ומראה לאיזה ערך הוא היה הופך, כך שאפשר לבדוק את הטענה ולא לסמוך עליה. אם אתם מכוונים למנתח YAML 1.2, החלפת הסכמה מסירה את אלה שרק 1.1 זקוק להן.
מה ההבדל בין YAML 1.1 ל-1.2 כאן?
‏1.2 ויתר על הפותרים שגורמים לרוב ההפתעות: ‏yes/no/on/off כבר אינם בוליאנים, בסיס 60 נעלם, ואין טיפוס חותמת זמן. ‏PyYAML מממש 1.1 ועדיין נמצא בכל מקום, ולכן הפלט השמרני הוא ברירת המחדל; הגדרת 1.2 קיימת למקרה שאתם יודעים מה יקרא את הקובץ.
האם הוא יכול להמיר YAML חזרה ל-JSON?
לא, במכוון. קורא YAML זקוק לעוגנים, כינויים, תגים, מפתחות מיזוג, חמישה סגנונות סקלר ושתי גרסאות סכמה, וטעות דקה באחד מהם פירושה לקבל קובץ ולהחזיר נתונים שונים ממה שהכיל. הכשל הזה שקט, וזה מה שעושה אותו גרוע מלא להציע את התכונה.
איך אני מקבל קובץ רב-מסמכים בסגנון Kubernetes?
הדליקו את המתג שפולט כל איבר של מערך עליון כמסמך משלו, והמערך הופך למסמכים מופרדים בשלושה מקפים. אם הקלט שלכם הוא NDJSON במקום — אובייקט JSON אחד בכל שורה, כפי שיומנים ויצואי API לעיתים קרובות — זה מזוהה אוטומטית ומדווח מעל הפלט.
למה אין הערות בפלט?
מפני שלא היו כאלה בקלט. הערות הן הדבר העיקרי שיש ל-YAML ואין ל-JSON, וממיר אינו יכול להמציא אותן. כדאי לזכור את זה גם בכיוון ההפוך: אם תעבירו קובץ YAML הלוך ושוב דרך JSON, כל הערה שבו נעלמה.
האם משהו שאני מדביק נשלח לשרת?
לא. הניתוח וההמרה רצים כולם בדפדפן שלכם; שום דבר אינו מועלה או נרשם, וזה עובד בלי חיבור רשת.