המרת JSON ל-YAML בשורת הפקודה: ארבע מלכודות מאחורי פקודת yq אחת
שורה תחתונה: הפקודה הכי מועתקת במדריכים, yq -P input.json > output.yaml, מפיקה בפועל JSON על yq v4. עם yq v4.44.6, PyYAML 6.0.3 ו-js-yaml על דגימה של 2.39 MB נמדדו ארבע קבוצות נתונים שמראות שפורמט הפלט נקבע לפי סיומת הקובץ, ומובאות הדרכים הנכונות להמרת באצ׳ים, גרשיים, הזחה ודיוק שלמים גדולים.

שורה תחתונה: yq -P input.json > output.yaml — הפקודה שמופיעה הכי הרבה במדריכים — עדיין מפיקה JSON על yq v4. כדי לקבל YAML חייבים לכתוב -o=yaml (או -oy) במפורש; -P הוא קיצור של --prettyPrint, והוא שולט בהזחה ובסגנון הבלוק, לא בפורמט הפלט.
המרת JSON ל-YAML מעולם לא הייתה עניין של "להחליף סיומת לקובץ". המחיר האמיתי מסתתר בארבעה מקומות: הסיומת של הקובץ קובעת את פורמט הפלט, כללי הגרשיים משכתבים בשקט את המחרוזות שלכם, הזחת הרצפים משפיעה על הכלים בהמשך, ומספר שלם גדול שעבר דרך yq מאבק דיוק לתמיד.
1. קודם מריצים את הפקודה הכי פופולרית
על מכונה נקייה עם yq v4.44.6 מותקן, בונים קלט מינימלי:
printf '{"a":1,"b":{"c":2}}' > s.json
yq -P '.' s.json
מה שחוזר הוא לא YAML, אלא JSON עם הזחה:
{
"a": 1,
"b": {
"c": 2
}
}
אם תפעלו לפי המדריך ותנתבו את זה ל-output.yaml, תקבלו קובץ עם סיומת .yaml שבתוכו JSON. כל מפענח YAML 1.2 קורא אותו ברצון (ב-YAML 1.2 JSON הוא תת-קבוצה — הנימוק המלא להכללה הזאת נמצא בגבולות המרת JSON ↔ YAML ו-XML), כך ששום דבר לא נשבר מיד — עד היום שבו מישהו מוסיף שם הערה או עוגן, או ש-yamllint ב-CI מתחיל לפלוט קיר שלם של truthy value should be one of.
הקריאה שבאמת מפיקה YAML היא זו שקובעת את פורמט הפלט במפורש:
yq -o=yaml '.' s.json
# a: 1
# b:
# c: 2
2. למה זה קורה: הסיומת קובעת את פורמט הפלט
yq v4 פועל לפי הכלל "פורמט הקלט קובע את פורמט הפלט". הוא מסיק את הפורמט מסיומת קובץ הקלט, וכשאין -o הוא משתמש באותה הסקה. קלט .json → פלט JSON; קלט .yaml → פלט YAML; בקריאה מ-stdin בלי סיומת, ברירת המחדל היא YAML.
תוצאות מדודות של ארבע דרכים על אותו קלט:
| פקודה | מקור הקלט | פורמט הפלט בפועל | תוצאה |
|---|---|---|---|
yq -P '.' s.json |
קובץ s.json |
JSON (עם הזחה) | מלכודת כשמנתבים ל-.yaml |
yq -o=yaml '.' s.json |
קובץ s.json |
YAML (סגנון בלוק) | נכון |
yq -P -o=yaml '.' s.json |
קובץ s.json |
YAML (סגנון בלוק) | נכון, -P מיותר כאן |
cat s.json | yq -P '.' |
stdin, בלי סיומת | YAML (סגנון בלוק) | נכון, כי ברירת המחדל היא yaml |
השורה השלישית אומרת דבר אחד: -P אינו הכרחי תמיד כש--o=yaml כבר קיים, אבל הוא גם אינו תמים — ראו את נתוני הזמן בחלק השישי. השורה הרביעית מסבירה למה "העתקתי מהמדריך וזה עבד": רבים מזינים נתונים דרך צינור, מה שבמקרה עוקף את ענף הסיומת לגמרי.
אפשר לפרק את המשמעות המעשית עוד קצת. נניח שבמאגר אחד יושבים זה לצד זה configs/prod.json ו-configs/base.yaml; אז אותה פקודת yq '.' תפיק פלט בפורמט שונה עבור שני הקבצים. אם מישהו יבצע commit לפלט הזה בחזרה, ה-diff יציג תערובת: קובץ אחד בסגנון JSON והשני בסגנון YAML. הפתרון פשוט — כתבו -o במפורש בכל פקודה, אל תסתמכו על ההסקה.
3. המרה בבאץ': שלוש דרכים במעטפת
המרה של קובץ אחד היא רק נקודת ההתחלה. הסיבה האמיתית לעשות את זה בשורת הפקודה היא כמעט תמיד באצ'ים: כל התצורות בתיקייה, כל תמונת מצב של API, בבת אחת.
לולאת bash / zsh (הכי ברורה, לתיקייה אחת):
for f in configs/*.json; do
yq -o=yaml '.' "$f" > "${f%.json}.yaml"
done
find רקורסיבי (כשגם תת-תיקיות נחשבות):
find . -name '*.json' -exec sh -c 'yq -o=yaml "." "$1" > "${1%.json}.yaml"' _ {} \;
PowerShell (על CI של Windows):
Get-ChildItem *.json | ForEach-Object { yq -o=yaml '.' $_.FullName > ($_.BaseName + '.yaml') }
לכל שלוש הדרכים יש תנאי מוקדם אחד נוקשה: קודם ודאו שה-JSON במקור תקין. ממיר אינו מפענח; כשיש שגיאת תחביר, ממירים רבים מספקים מיקום מעורפל במקום פירוט ברמת השורה שמאמת נותן. הוספת שמירה אחת בתוך הלולאה כמעט לא עולה דבר:
for f in configs/*.json; do
jq empty "$f" || { echo "invalid: $f"; continue; }
yq -o=yaml '.' "$f" > "${f%.json}.yaml"
done
jq empty רק מפענח ולא מדפיס, ועל JSON לא תקין מחזיר קוד יציאה שאינו אפס. בצירוף set -euo pipefail, כל הצינור נעצר על הקובץ הפגום הראשון במקום להפיק ערימה של פלט חצי-מוכן.
עוד נקודה שכדאי לזכור: הלולאה for f in configs/*.json בטוחה כשיש רווחים בשמות הקבצים, כי "$f" מצוטט; אבל אם שום קובץ לא מתאים, ה-glob נכנס ללולאה כמחרוזת המילולית configs/*.json. במאגרים גדולים בטוח יותר לבחור ב-shopt -s nullglob או בגישה מבוססת find.
4. ארבעה פרמטרי פלט, ארבע משימות נפרדות
רשימת הפרמטרים של yq ארוכה, אבל בדרך של JSON → YAML צריך לזכור באמת רק ארבעה:
| פרמטר | מה הוא עושה | מתי חייבים לכתוב אותו |
|---|---|---|
-o=yaml / -oy |
קובע את פורמט הפלט ל-YAML | כשהקלט הוא קובץ .json |
-P / --prettyPrint |
כופה סגנון בלוק ומסדיר הזחה | בקריאה מ-stdin, או כשהזחת רצפים חשובה |
-I=n |
מספר רווחי ההזחה, ברירת מחדל 2 | כשבהמשך דורשים 4 רווחים (כמה מדריכי סגנון של Ansible) |
--split-exp |
מפצל איברי מערך למסמכים | כשיוצרים מניפסטים מרובי משאבים של Kubernetes |
סעיף המסמכים המרובים ראוי להערה נפרדת. במניפסטים של Kubernetes מקובל להפריד משאבים ב----. כשיוצרים את זה ממערך JSON:
yq -P -o=yaml '.[] | splitDoc' --split-exp 'true' kubernetes-list.json
בלי --split-exp תקבלו רצף YAML אחד; איתו, כל איבר מערך הופך למסמך עצמאי שמופרד ב----. kubectl apply יקבל את שני הקבצים, אבל הקריאות של ה-diff שונה בסדר גודל.
5. גרשיים: שישה סוגי מחרוזות שחייבים גרשיים
הסקת הטיפוסים ב-YAML היא משתמעת. ערך מחרוזת שנראה כמו מספר או בוליאני יפוענח ככזה אלא אם כן הוא בגרשיים. זה לא סיכון תיאורטי: במדידה על yq v4.44.6, כל ששת הסוגים הבאים מקבלים גרשיים אוטומטית, מה שמוכיח שהמפענח באמת היה טועה בהם:
printf '{"version":"1.0","enabled":"true","port":"8080","empty":"","colon":"host: value","hash":"v # c","unicode":"端口 8080","ts":"2026-11-03"}' | yq -P -o=yaml '.'
version: "1.0"
enabled: "true"
port: "8080"
empty: ""
colon: 'host: value'
hash: 'v # c'
unicode: 端口 8080
ts: "2026-11-03"
ההשלכות, אחת אחת:
| הערך המקורי (מחרוזת JSON) | איך זה יפוענח בלי גרשיים | השפעה מעשית |
|---|---|---|
"1.0" |
float 1.0 |
מספר גרסה הופך לעשרוני; 1.10 יהפוך ל-1.1 |
"true" |
בוליאני true |
לוגיקת התנאי מתהפכת או שהטיפוסים לא תואמים |
"8080" |
שלם 8080 |
הטיפוס של הפורט משתנה, השוואת מחרוזות נכשלת |
"" |
null |
המחרוזת הריקה נעלמת, בהמשך מקבלים None |
"host: value" |
שגיאת פענוח או מיפוי מקונן | אחרי הנקודתיים חייב לבוא רווח, אחרת שגיאת תחביר |
"2026-11-03" |
אובייקט תאריך | מפוענח ל-datetime.date ולא למחרוזת |
האחרון הוא הערמומי מכולם. ב-JSON תאריך הוא פשוט מחרוזת; אחרי ההמרה הוא הופך לטיפוס תאריך של YAML, ומימושים רבים מסריאלים אותו חזרה כאובייקט תאריך ולא כמחרוזת המקורית — העקביות של הסבב הלוך ושוב נשברת. גם PyYAML וגם yq בוחרים להוסיף גרשיים לערכים האלה, וזה כמו הודאה שהסיכון אמיתי.
שימו לב גם ש-unicode: 端口 8080 יוצא בלי גרשיים — תווים סיניים בטוחים ב-YAML. מה שצריך להדאיג אתכם אינו זה, אלא סדר המפתחות והדיוק שמופיעים בחלק הבא. עם זאת, אזהרה אחת: אם העורך או המסוף שלכם לא מטפלים בקידוד כראוי, עבודה עם מחרוזות רב-לשוניות עלולה להפיק התנהגות מוזרה — ודאו תמיד UTF-8 ב-LANG.
6. המקבילות ב-Python וב-Node וזמני המדידה
לא על כל מכונה אפשר להתקין את yq (סביבות מנותקות, תמונות CI נעולות, Windows בלי מנהל חבילות). למקבילות ב-Python וב-Node יש ברירות מחדל משלהן שנושכות. טבלת השוואה מלאה לבחירה בין שלוש הדרכים לפי תרחיש נמצאת בבחירת כלי שורת פקודה ועורכים ל-JSON; המאמר הזה מתמקד רק בכיוון של JSON → YAML.
Python + PyYAML (שלושה ארגומנטים, אף אחד מהם אופציונלי):
import json, yaml
with open("config.json", encoding="utf-8") as f:
data = json.load(f)
with open("config.yaml", "w", encoding="utf-8") as f:
yaml.safe_dump(
data, f,
default_flow_style=False, # סגנון בלוק, לא {a: 1, b: 2}
allow_unicode=True, # התווים לא יברחו ל-\u05D0\u05D1
sort_keys=False, # סדר המפתחות המקורי של JSON נשמר
)
ברירת המחדל של השלושה היא False / False / True, כלומר PyYAML מהקופסה מסדר את המפתחות בסדר אלפביתי ומבריח תווים שאינם לטיניים ל-\uXXXX. על אותו קלט, עם פרמטרי ברירת המחדל, הפלט הוא:
apiVersion: v1
data:
LOG_LEVEL: info
colon: 'host: value'
unicode: "\u7AEF\u53E3 8080"
kind: ConfigMap
metadata:
labels:
app: web
name: app-config
שימו לב ש-kind עבר אחרי apiVersion ולפני metadata. המוסכמה בקהילת Kubernetes היא ש-apiVersion יופיע לפני kind; שיבוש סדר המפתחות לא שובר את הפענוח, אבל הוא הופך את הסקירה לייסורים.
Node + js-yaml:
const fs = require("fs");
const yaml = require("js-yaml");
const data = JSON.parse(fs.readFileSync("config.json", "utf8"));
fs.writeFileSync(
"config.yaml",
yaml.dump(data, { lineWidth: -1, noRefs: true, sortKeys: false })
);
lineWidth: -1 מכבה את קיפול השורות הארוכות (אחרת כתובות URL ארוכות נשברות באמצע), ו-noRefs: true מונע יצירת עוגנים וכינויים — חובה לכבות כשהצרכן הוא kubectl או כלי אחר שלא פותר אותם.
זמנים מדודים (דגימה: JSON של 2.39 MB, 12000 רשומות, כל אחת עם אובייקט מקונן ושני מערכים; המכונה היא סביבת הארגז החול ששימשה למשימה הזאת):
| מימוש | זמן המרה | זמן פענוח חזרה | סבב הלוך ושוב |
|---|---|---|---|
yq -o=yaml v4.44.6 |
0.45 s | 0.45 s | זהה |
yq -P -o=yaml v4.44.6 |
4.16 s | — | זהה |
| Node js-yaml 4.x | 0.23 s | 0.11 s | זהה |
| Python PyYAML 6.0.3 | 3.26 s | 8.01 s | זהה |
שתי תוצאות מנוגדות לאינטואיציה. ראשית, הוספת -P מאטה את yq פי 9 בערך (0.45 s → 4.16 s) בעוד שבדגימה הזאת מספר הבתים בפלט זהה לגמרי (2,326,895 בתים בשני המקרים) — -P מבצע מעבר עיצוב מחדש מלא, וזה מחיר שמצטבר כשממירים כמה מאות קבצים. שנית, הפענוח ההפוך של PyYAML (8.01 s) איטי יותר מפי שניים מההמרה הקדימה שלו (3.26 s), ולכן Python אינה בחירה טובה אם הצינור שלכם קורא YAML חזרה שוב ושוב.
עוד נתון: 2.39 MB של JSON הפכו ל-2.33 MB של YAML בסגנון בלוק, כלומר כ-3% יותר קטן. האמרה העממית ש-"YAML תמיד גדול מ-JSON" אינה נכונה — בעומק הקינון, הגרשיים, הפסיקים והסוגריים המסולסלים של JSON יכולים לתפוס יותר נפח מרווחי ההזחה של YAML.
7. שלושה דברים שלא חוזרים
החלקים הקודמים דיברו על "איך להמיר נכון". החלק הזה מדבר על "מה אובד". מדידה עם קלט שנבנה במיוחד:
printf '{"order_id":12345678901234567890,"zip":"0150","ts":"2026-11-03","f":1.0e6}' > t.json
yq -o=yaml '.' t.json
הפלט של yq:
order_id: 1.2345678901234567e+19
zip: "0150"
ts: "2026-11-03"
f: 1000000
ומומר חזרה ל-JSON: {"order_id":12345678901234567000,"zip":"0150","ts":"2026-11-03","f":1000000}.
מזהה הזמנה בן 20 ספרות, 12345678901234567890, הפך ל-12345678901234567000 — שלוש הספרות האחרונות התאפסו, בלי שום אזהרה. הסיבה היא ש-yq עובר דרך נתח ה-float64 של Go; שלמים מעבר ל-2^53 אינם ניתנים לייצוג מדויק, לכן הוא עובר לסימון מדעי, וכשחוזרים הערך המקורי כבר איננו.
אותו קלט דרך PyYAML:
order_id: 12345678901234567890
zip: '0150'
ts: '2026-11-03'
f: 1000000.0
השלמים של Python הם בעלי דיוק שרירותי, ולכן כל 20 הספרות שורדות וגם בקריאה חזרה מתקבל 12345678901234567890. אם בנתונים שלכם יש מזהי הזמנות, מזהי snowflake או מספרי זהות, אל תשתמשו ב-yq — השתמשו ב-Python, או הפכו את השדה למחרוזת קודם. המנגנון המלא של סוג הזה של בעיות דיוק מוסבר בלמה שלמים גדולים מאבדים דיוק ב-JSON.
האובדן השני הוא ההבחנה בין float ל-int: 1.0e6 הופך ל-1000000 בפלט של yq, בעוד PyYAML שומר 1000000.0. השלישי הוא סדר המפתחות — PyYAML מסדר כברירת מחדל, yq ו-js-yaml שומרים כברירת מחדל, וזה מקור נסתר למחלוקת כשצוותים בשפות שונות עובדים יחד.
בפרויקט אמיתי, הראשון מבין השלושה הוא ההרסני ביותר, כי הוא שקט. מזהי הזמנות, מזהי טרנזקציות, מזהי snowflake — גם כשכמה ספרות אחרונות משתנות, הקובץ נראה לעין תקין לגמרי. לכן הרצה של בדיקת סבב הלוך ושוב על קבוצת דוגמאות קטנה לפני ההמרה חוסכת שעות של ניפוי באגים בעתיד.
8. ב-CI אל תסתפקו בהמרה: אימות → המרה → lint
להתייחס להמרה כאל השלב היחיד הוא הפגם הנפוץ ביותר בצינורות. קוד יציאה 0 אחרי המרה אומר רק ש"ה-JSON של הקלט פוענח", לא שהפלט נכון ולא שהפלט עומד בדרישות של הצרכן. הצורה הנכונה בת שלושה שלבים:
#!/usr/bin/env bash
set -euo pipefail
# 1. אימות המקור: שכבת התחביר
jq empty deployment.json
# 2. המרה: קביעת פורמט הפלט במפורש
yq -o=yaml '.' deployment.json > deployment.yaml
# 3. אימות התוצר: קריאה חזרה על ידי מפענח YAML, ועובר lint
yq -o=json -I=0 '.' deployment.yaml > /dev/null
yamllint deployment.yaml
השורה yq -o=json ... > /dev/null בשלב 3 נראית מיותרת והיא השורה השימושית ביותר בכל הבלוק: היא מאלצת את התוצר להיות מפוענח מחדש. שגיאות הזחה, כשלון בפתרון עוגנים ושכתוב של טיפוסים — כולם נחשפים כאן. אם תדלגו עליה, הבעיה תמתין עד kubectl apply או עד שהאפליקציה תעלה.
אם התוצר הוא תצורה שמתוחזקת ביד (ערכי Helm, Docker Compose, תהליכי עבודה של GitHub Actions), ודאו עוד שלושה דברים: סדר המפתחות לא שובש, המספרים הארוכים לא איבדו ספרות, והערות עדיין צריך להוסיף ידנית — ב-JSON אין הערות, ולכן כל הערה חייבת להיכתב אחרי ההמרה; שום ממיר לא ימציא אותן במקומכם.
שאלות נפוצות
yq -P או -o=yaml — מה באמת לכתוב?
-o=yaml קובע אם הפלט הוא YAML בכלל; -P קובע איך ה-YAML נראה. כשהקלט הוא קובץ .json, -o=yaml הוא חובה, ו--P נדרש רק כשאתם רוצים סגנון בלוק כפוי או הזחת רצפים. בקריאה מ-stdin בלי סיומת, yq כבר מפיק YAML כברירת מחדל, ולכן -P לבדו מספיק.
אפשר להזין את ה-YAML שהומר ישר ל-Kubernetes?
מבחינה תחבירית כן, אבל עם שלושה תנאים: לוודא שמפתחות עם מוסכמת סדר כמו apiVersion לא סודרו מחדש (PyYAML מסדר כברירת מחדל; קבעו sort_keys=False), לוודא שלא נוצרו עוגנים וכינויים (ב-js-yaml: noRefs: true), ולוודא שהמספרים הארוכים שמרו על הספרות שלהם. למניפסטים מרובי משאבים צריך גם לפצל למסמכים מופרדים ב---- באמצעות --split-exp.
למה ה-YAML שהמרתי יצא בשורה אחת, {a: 1, b: 2}, ולא מוזח?
זה סגנון flow. הוא חוקי, אבל הוא מבטל את הטעם שבהמרה. השתמשו ב--P כדי לכפות סגנון בלוק; עם PyYAML, ודאו ש-default_flow_style=False (הערך הזה הוא ברירת המחדל מאז PyYAML 5.1, בגרסאות ישנות צריך לכתוב אותו במפורש).
מזהה הזמנה בן 20 ספרות איבד את הספרות האחרונות ב-YAML. אפשר להציל אותן?
אי אפשר לשחזר, אפשר רק למנוע מראש. yq מפענח דרך float64, ולכן שלמים מעבר ל-2^53 מאבדים דיוק מטבע הדבר. או שתעברו ל-Python (שלמים בדיוק שרירותי; במדידה שלנו נשמרו במלואם), או שתשמרו מספרים ארוכים כמחרוזות כבר ב-JSON — האפשרות השנייה בטוחה יותר, כי היא פותרת בו-זמנית גם את הבעיה המקבילה של JSON.parse בצד ה-JavaScript.
כלי המרה מקוון או שורת פקודה?
עבור תצורות רגישות ותמונות מצב של API עם טוקנים, השתמשו בשורת פקודה מקומית או בכלי שרץ בצד הלקוח בלבד — אל תעלו. לשימוש חד-פעמי, בנפח קטן, כשצריך אישור חזותי, כלי מקוון מהיר יותר; העמוד /convert/yaml באתר הזה רץ כולו בדפדפן והנתונים לא עוזבים את המכונה שלכם. לכל דבר שהוא באצ'ים, שנכנס ל-CI או שצריך להיות ניתן לשחזור — שורת הפקודה בלבד, וגם שלבי האימות נכתבים באותו סקריפט.