แปลง JSON เป็น YAML บนบรรทัดคำสั่ง: กับดักสี่อย่างที่ซ่อนอยู่หลังคำสั่ง yq เพียงคำสั่งเดียว
สรุปสำคัญ: คำสั่งที่ถูกคัดลอกมากที่สุดในทุกบทเรียนคือ yq -P input.json > output.yaml แต่บน yq v4 มันยังให้ JSON อยู่ บทความนี้วัด yq v4.44.6, PyYAML 6.0.3 และ js-yaml บนตัวอย่าง 2.39 MB อธิบายว่าทำไมนามสกุลของอินพุตเป็นตัวกำหนดรูปแบบเอาต์พุต และให้สูตรที่ถูกต้องสำหรับการแปลงเป็นชุด เครื่องหมายคำพูด การย่อหน้า และความแม่นยำของจำนวนเต็มใหญ่

สรุปสั้น: yq -P input.json > output.yaml คือคำสั่งที่ถูกคัดลอกมากที่สุดในบทเรียนทั้งหลาย แต่บน yq v4 มันยังคงผลิต JSON อยู่ ถ้าอยากได้ 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 ทุกตัวอ่านได้สบาย ๆ เพราะ JSON เป็นซูเบตของ YAML 1.2 และการพิสูจน์ความสัมพันธ์นี้แบบครบถ้วนอยู่ใน ขอบเขตการแปลง JSON, YAML และ XML ดังนั้นจะยังไม่มีข้อผิดพลาดเกิดขึ้นทันที มันจะพังในวันที่มีคนเพิ่มคอมเมนต์หรือ anchor เข้าไป หรือเมื่อ yamllint ใน CI เริ่มพ่นข้อความ truthy value should be one of ออกมาเป็นกำแพง
ข้อผิดพลาดนี้ยิ่งเงียบในทีมที่มีไปป์ไลน์ปล่อยงานอยู่แล้ว ไฟล์ถูกสร้างใน job หนึ่ง ถูกเก็บเข้ามาเป็นเดือน ๆ และทุกคนเข้าใจว่าเป็น YAML เพราะมันลงท้ายด้วย .yaml โดยไม่มีใครเปิดดูข้างใน สัญญาณเตือนดังขึ้นก็ต่อเมื่อมีคนเพิ่ม yamllint เข้าไปป์ไลน์ หรือเมื่อเพื่อนร่วมทีมพยายามเขียนคำอธิบายด้วย # แล้วตัวแยกวิเคราะห์ฟ้องไวยากรณ์ที่ใน JSON ไม่มีอยู่เลย ตอนนั้น artifact ที่สร้างผิดรูปแบบไปแล้วเป็นสิบชิ้น และต้องสั่งสร้างใหม่ทั้งหมด
คำสั่งที่ผลิต 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 |
แถวที่สามบอกเรื่องสำคัญว่า เมื่อมี -o=yaml อยู่แล้ว -P ไม่จำเป็นเสมอไป แต่มันก็ไม่ได้ไม่มีโทษ ดูตัวเลขเวลาในหัวข้อที่หก ส่วนแถวที่สี่อธิบายว่าทำไมคำว่า "ก็อปมาแล้วใช้ได้" ถึงเป็นจริงสำหรับคนจำนวนมาก เพราะหลายคนป้อนข้อมูลผ่าน pipe ซึ่งบังเอิญเลี่ยงสาขาที่ตรวจนามสกุลไปพอดี
ควรเน้นความต่างจาก jq ไว้ด้วย jq คืน JSON เสมอไม่ว่าไฟล์จะมีนามสกุลอะไร คนที่มาจาก jq จึงเคยชินกับสมมติฐานว่าตัวแปลง "รู้อยู่แล้ว" ว่าอยากได้รูปแบบไหน และนี่แหละคือเหตุผลที่ไม่มีใครสงสัย -P ในทางกลับกัน คนที่ใช้ cat ไฟล์ | yq จะไม่มีวันเจอปัญหาเลย เพราะเส้นทางนั้น yq ตั้งสมมติฐานเป็น YAML ไว้ก่อนแล้ว การที่สองเส้นทางให้ผลต่างกันคือเหตุผลที่กับดักนี้ยังอยู่รอดในบทเรียนมากมาย ผู้อ่านแต่ละคนลองคนละรูปแบบ ทั้งสองแบบ "ทำงาน" เหมือนกัน แต่มีเพียงแบบเดียวที่ทำงานเพราะเหตุผลที่ถูกต้อง
3. การแปลงเป็นชุด: สูตร shell สามแบบ
การแปลงไฟล์เดียวเป็นเพียงจุดเริ่มต้น เหตุผลจริงเกือบทุกอย่างที่จะทำบนบรรทัดคำสั่งล้วนเป็นงานเป็นชุด ทั้งไฟล์คอนฟิกในไดเรกทอรี ทั้งภาพรวม 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 ไปป์ไลน์จะหยุดที่ไฟล์เสียไฟล์แรก แทนที่จะผลิตเอาต์พุตครึ่ง ๆ กลาง ๆ ออกมาเป็นกอง ตัวตรวจสอบที่อิง schema มักบอกเส้นทางของฟิลด์ด้วย instancePath ซึ่งตัวแปลงแทบไม่เคยให้ความแม่นยำขนาดนั้น และนี่คือเหตุผลที่การเช็กก่อนไม่ใช่ของฟุ่มเฟือย แต่คือตาข่ายนิรภัย
รายละเอียดเล็ก ๆ สองอย่างที่สร้างปัญหาในงานจริงบ่อยที่สุด อย่างแรกคือเครื่องหมายคำพูดรอบ "$f" ถ้าไม่ใส่ ชื่อไฟล์ที่มีช่องว่างจะทำให้เส้นทางขาดและลูปเขียนลงที่ผิด อย่างที่สองคือ ${f%.json}.yaml ซึ่งตัดส่วนท้ายจากด้านขวา ถ้าไฟล์ชื่อ config.prod.json ผลจะเป็น config.prod.yaml ซึ่งตรงตามที่ต้องการพอดี แต่ควรลองเช็กสักครั้งก่อนปล่อยลูปไปบนไฟล์หลายร้อยชิ้น
4. พารามิเตอร์เอาต์พุตสี่ตัว หน้าที่คนละอย่าง
รายการพารามิเตอร์ของ yq ยาวมาก แต่บนเส้นทาง JSON ไป YAML มีเพียงสี่ตัวที่ต้องจำ:
| พารามิเตอร์ | ทำอะไร | เมื่อไหร่ที่ต้องใส่ |
|---|---|---|
-o=yaml / -oy |
กำหนดรูปแบบเอาต์พุตเป็น YAML | ทุกครั้งที่อินพุตเป็นไฟล์ .json |
-P / --prettyPrint |
บังคับรูปแบบบล็อกและจัดการย่อหน้าให้สม่ำเสมอ | อ่านจาก stdin หรือเมื่อการย่อหน้าของลำดับสำคัญ |
-I=n |
ความกว้างของการย่อหน้า ค่าเริ่มต้น 2 | ปลายทางต้องการ 4 ช่องว่าง เช่น คู่มือสไตล์ของ Ansible บางชุด |
--split-exp |
แยกสมาชิกของอาร์เรย์เป็นหลายเอกสาร | ตอนสร้างมานิเฟสต์ Kubernetes หลาย resource |
กรณีหลายเอกสารสมควรแยกอธิบาย มานิเฟสต์ของ Kubernetes นิยมคั่น resource ด้วย --- ตามธรรมเนียม การสร้างจากอาร์เรย์ JSON ทำได้ดังนี้:
yq -P -o=yaml '.[] | splitDoc' --split-exp 'true' kubernetes-list.json
ถ้าไม่ใส่ --split-exp จะได้ลำดับ YAML อันเดียว พอใส่ สมาชิกแต่ละตัวของอาร์เรย์จะกลายเป็นเอกสารอิสระที่คั่นด้วย --- kubectl apply รับได้ทั้งสองแบบ แต่ความอ่านง่ายของ diff ต่างกันคนละระดับเลย ในการรีวิวโค้ด อาร์เรย์ก้อนใหญ่บังคับให้คนนับวงเล็บปีกกา ขณะที่เอกสารแยกกันโชว์ resource แต่ละตัวได้ครบ
พารามิเตอร์ -I=n เป็นตัวที่ถูกลืมมากที่สุด yamllint และ playbook ของ Ansible จำนวนมากกำหนดการย่อหน้าไว้ชัดเจน ถ้าสร้าง YAML ด้วย 2 ช่องว่างแต่ที่เก็บใช้ 4 ช่องว่าง ไฟล์จะยังถูกต้องตามไวยากรณ์แต่ lint ปฏิเสธตั้งแต่ commit แรก การตรึง -I=4 ไว้ในสคริปตแปลงช่วยเลี่ยง 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" |
จำนวนทศนิยม 1.0 |
หมายเลขเวอร์ชันกลายเป็นทศนิยม และ 1.10 จะย่อลงเป็น 1.1 |
"true" |
บูลีน true |
ตรรกะเงื่อนไขกลับด้านหรือชนิดไม่ตรงกัน |
"8080" |
จำนวนเต็ม 8080 |
ชนิดของพอร์ตเปลี่ยนและการเปรียบเทียบสตริงล้มเหลว |
"" |
null |
สตริงว่างหายไป ปลายทางได้ None แทน |
"host: value" |
ข้อผิดพลาดของการแยกวิเคราะห์ หรือ map ซ้อน | หลังเครื่องหมายทวิภาคต้องมีช่องว่างเสมอ |
"2026-11-03" |
object วันที่ | ถูกทำให้กลับเป็น datetime.date ไม่ใช่สตริง |
ข้อสุดท้ายโหดที่สุด ใน JSON วันที่เป็นแค่สตริง พอแปลงแล้วกลายเป็นวันที่ของ YAML และหลาย implementation ตอนแปลงกลับจะส่งออกเป็น object วันที่แทนสตริงเดิม ทำให้ความสอดคล้องของการไปกลับขาดตอน ทั้ง PyYAML และ yq เลือกใส่เครื่องหมายคำพูดให้ค่ากลุ่มนี้ ซึ่งเท่ากับยอมรับว่าความเสี่ยงมีจริง
อีกอย่างหนึ่ง บรรทัด unicode: 端口 8080 ไม่ได้ใส่เครื่องหมายคำพูดเลย ตัวอักษรจีนปลอดภัยใน YAML สิ่งที่ต้องกังวลไม่ใช่ตรงนี้ แต่เป็นลำดับคีย์และความแม่นยำซึ่งจะพูดถึงถัดไป
สำหรับคอนฟิกที่ทีมไทยเขียนมีกรณีเพิ่มเติมที่ไม่ได้อยู่ในตารางแต่ควรระวังเป็นพิเศษ ค่าอย่าง ใช่ กับ ไม่ใช่ วันที่ในรูปแบบ 03/11/2026 และรหัสที่มีศูนย์นำหน้า เช่น รหัสไปรษณีย์ 0150 หรือเลขบัตรประชาชน 13 หลัก กับตัวแยกวิเคราะห์ YAML 1.1 ซึ่งยังมีอยู่ในไลบรารีเก่าบางตัว สตริง no จะกลายเป็นบูลีน false ฟิลด์อย่าง active: no จึงเปลี่ยนความหมายโดยที่ไม่มีใครแตะโค้ด วิธีรับมือก็คือใส่เครื่องหมายคำพูดให้ทุกค่าที่ "ดูเหมือนอย่างอื่น" และอย่าเชื่อว่าตัวแปลงจะเดาความตั้งใจของคุณได้ รหัสที่มีเลขศูนย์นำหน้าก็เข้าข่ายเดียวกัน ถ้าไม่ใส่เครื่องหมายคำพูดจะกลายเป็นตัวเลขแล้วศูนย์หายไป
6. สูตรเทียบเท่าใน Python และ Node พร้อมเวลาที่วัดได้
ติดตั้ง yq ได้ไม่ทุกที่ เครื่องออฟไลน์ อิมเมจ CI ที่ถูกล็อก Windows ที่ไม่มีตัวจัดการแพ็กเกจ ล้วนเจอปัญหานี้ สูตรเทียบเท่าใน Python และ Node มีอยู่ แต่แต่ละทางก็มีค่าตั้งต้นของตัวเองที่รอจะกัด ถ้าอยากดูตารางเปรียบเทียบครบทั้งสามเส้นทางแยกตามสถานการณ์ ดูที่ การเลือกเครื่องมือ CLI และ editor 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, # block style, not {a: 1, b: 2}
allow_unicode=True, # keep CJK as-is, not \u7AEF\u53E3
sort_keys=False, # preserve the original key order
)
ค่าตั้งต้นของสามตัวนี้คือ False / False / True ซึ่งหมายความว่า PyYAML ที่เพิ่งติดตั้งจะเรียงคีย์ตามตัวอักษรและ escape ตัวอักษรจีนเป็น \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 การสลับลำดับไม่ได้ทำให้การแยกวิเคราะห์พัง แต่ทำให้คนรีวิวลำบากมาก คนที่อ่านมานิเฟสต์คาดหวังลำดับที่คุ้นเคย และ diff ที่สลับคีย์ทุกตัวจะดูใหญ่กว่าความเป็นจริงหลายเท่า
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 ห้ามสร้าง anchor และ alias ซึ่งจำเป็นต้องปิดเมื่อปลายทางคือ kubectl หรือเครื่องมืออื่นที่ไม่คลี่ anchor
เวลาที่วัดได้ ตัวอย่างคือ JSON 2.39 MB, 12000 เรกคอร์ด แต่ละตัวมี object ซ้อนอยู่และอาร์เรย์สองตัว เครื่องที่ใช้คือ sandbox ที่ใช้ทำงานชิ้นนี้:
| การทำงาน | เวลาแปลง | เวลาแยกวิเคราะห์กลับ | การไปกลับ |
|---|---|---|---|
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 ต้องจัดหน้าเอกสารใหม่ทั้งรอบ และต้นทุนนี้จะสะสมเมื่อแปลงไฟล์หลายร้อยไฟล์ใน job เดียว อย่างที่สอง การแยกวิเคราะห์ย้อนกลับของ PyYAML ที่ 8.01 s ช้ากว่าการดัมป์ไปข้างหน้าที่ 3.26 s เกินสองเท่า ดังนั้นถ้าไปป์ไลน์ต้องอ่าน YAML กลับมาซ้ำ ๆ Python ไม่ใช่ตัวเลือกที่ดี
อีกอย่างหนึ่ง JSON 2.39 MB กลายเป็น YAML แบบบล็อก 2.33 MB คือ เล็กลง ราว 3% ความเชื่อลอย ๆ ที่ว่า "YAML ต้องใหญ่กว่า JSON เสมอ" จึงไม่เป็นจริง เพราะในโครงสร้างที่ซ้อนลึกมาก เครื่องหมายคำพูด ลูกน้ำ และวงเล็บปีกกาของ JSON อาจกินพื้นที่มากกว่าช่องว่างจากการย่อหน้าของ YAML เสียอีก ในทางปฏิบัติ สภาพแวดล้อมมักเป็นคนเลือกเส้นทางให้ก่อนตารางเวลาเสียอีก ถ้า runner มี Node ติดอยู่แล้ว js-yaml เร็วที่สุดและไม่ต้องโหลดไบนารีเพิ่ม แต่ถ้าทีมทำงานออฟไลน์และมีแค่ Python ที่ติดมากับเครื่อง PyYAML คือทางเดียวที่เหลือ และต้องยอมรับต้นทุนตอนอ่านกลับ
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 id หรือเลขบัตรประชาชน อย่าใช้ yq ให้ใช้ Python หรือเปลี่ยนฟิลด์นั้นให้เป็นสตริงตั้งแต่แรก กลไกทั้งหมดของปัญหาความแม่นยำกลุ่มนี้อธิบายไว้ใน ทำไมจำนวนเต็มใหญ่ใน JSON ถึงเสียความแม่นยำ
สิ่งที่หายไปอย่างที่สองคือความแตกต่างระหว่าง float กับ int เพราะ 1.0e6 กลายเป็น 1000000 ในเอาต์พุตของ yq ขณะที่ PyYAML เก็บ 1000000.0 ไว้ อย่างที่สามคือลำดับคีย์ PyYAML เรียงใหม่เป็นค่าตั้งต้น ส่วน yq กับ js-yaml เก็บลำดับเดิมไว้ นี่คือแหล่งความเห็นไม่ลงรอยกันที่ซ่อนอยู่ในทีมที่ใช้หลายภาษาพร้อมกัน
สำหรับร้านค้าออนไลน์และผู้ให้บริการชำระเงิน เรื่องนี้ยิ่งอ่านแล้วอึดอัด หมายเลขคำสั่งซื้อหรือรหัสธุรกรรม 20 หลักไม่เด่นใน diff เพราะความต่างมีแค่ศูนย์สามตัวท้าย และไฟล์ยังเป็น YAML ที่ถูกต้องอยู่ดี การป้องกันที่เชื่อถือได้มีทางเดียวคือเขียนเทสต์ไปกลับที่เปรียบเทียบ JSON ต้นฉบับกับผลลัพธ์ของ JSON → YAML → JSON แบบฟิลด์ต่อฟิลด์ แล้วให้ล้มเหลวทันทีที่ตัวเลขเปลี่ยนแม้เพียงหลักเดียว การเทียบแค่จำนวนไบต์หรือจำนวนฟิลด์ไม่พอ เพราะในกรณีนี้ไฟล์ทั้งสองมีขนาดเท่ากันเป๊ะ
8. ใน CI อย่าแค่แปลง: ตรวจสอบ → แปลง → lint
การมองว่าการแปลงเป็นขั้นตอนเดียวคือข้อบกพร่องของไปป์ไลน์ที่พบบ่อยที่สุด รหัสออกเป็นศูนย์หมายความว่า "JSON อินพุตแยกวิเคราะห์ได้" ไม่ได้หมายความว่า "เอาต์พุตถูกต้อง" และไม่ได้หมายความว่า "เอาต์พุตตรงตามที่ปลายทางต้องการ" รูปแบบที่ถูกต้องคือสามชั้น:
#!/usr/bin/env bash
set -euo pipefail
# 1. Validate the source: syntax layer
jq empty deployment.json
# 2. Convert: name the output format explicitly
yq -o=yaml '.' deployment.json > deployment.yaml
# 3. Validate the artifact: parseable YAML, and lint-clean
yq -o=json -I=0 '.' deployment.yaml > /dev/null
yamllint deployment.yaml
บรรทัด yq -o=json ... > /dev/null ในขั้นที่ 3 ดูเหมือนเกินความจำเป็น แต่เป็นบรรทัดที่มีประโยชน์ที่สุดในบล็อกนี้ เพราะ มันบังคับให้แยกวิเคราะห์ artifact ซ้ำอีกรอบ ความผิดพลาดเรื่องการย่อหน้า การคลี่ anchor ไม่สำเร็จ และชนิดข้อมูลที่ถูกเขียนใหม่จะโผล่ตรงนี้เอง ถ้าข้ามบรรทัดนี้ ปัญหาจะรอไปจนถึงตอน kubectl apply หรือตอนที่แอปเริ่มทำงาน ซึ่งก็คือตอนที่ปล่อยงานไปแล้ว
ถ้า artifact เป็นคอนฟิกที่คนดูแลเอง เช่น values ของ Helm, docker-compose หรือ workflow ของ GitHub Actions ต้องยืนยันเพิ่มอีกสามอย่าง คือลำดับคีย์ไม่ถูกสลับ ตัวเลขยาวไม่เสียหลัก และคอมเมนต์ยังต้องเติมด้วยมือ เพราะ JSON ไม่มีคอมเมนต์ คำอธิบายทุกอย่างจึงใส่ได้หลังการแปลงเท่านั้น และตัวแปลงจะไม่มีวันสร้างมันให้คุณเอง
ใน workflow ของ GitHub Actions รูปแบบนี้เข้ากันได้ไม่มีสะดุด job ตรวจสอบรันบน runner สะอาด jq กับ yamllint ติดตั้งในขั้นเดียวกัน และสคริปต์จะปล่อย artifact ก็ต่อเมื่อสามชั้นผ่านหมด การล็อกเวอร์ชันของเครื่องมือ รวมถึงเวอร์ชันของ yq ไว้ใน workflow คุ้มค่ามาก เพราะพฤติกรรมที่อธิบายในหัวข้อที่สองเป็นของ yq v4 และไม่จำเป็นต้องเป็นพฤติกรรมของเวอร์ชันที่ runner จะติดตั้งให้โดยอัตโนมัติในปีหน้า
คำถามที่พบบ่อย
ควรใช้ yq -P หรือ -o=yaml กันแน่
-o=yaml ตัดสินว่าเอาต์พุต เป็น YAML หรือไม่ ส่วน -P ตัดสินว่า YAML มีหน้าตาอย่างไร กับอินพุต .json ต้องใส่ -o=yaml เสมอ และ -P จำเป็นเฉพาะตอนที่อยากบังคับรูปแบบบล็อกหรือการย่อหน้าของลำดับ ถ้าอ่านจาก stdin โดยไม่มีนามสกุล yq ให้ YAML เป็นค่าตั้งต้นอยู่แล้ว ดังนั้น -P ตัวเดียวก็เพียงพอ
YAML ที่แปลงแล้วส่งให้ Kubernetes ได้เลยไหม
ในเชิงไวยากรณ์ได้ แต่มีเงื่อนไขสามข้อ คีย์ที่มีธรรมเนียมเรื่องลำดับเช่น apiVersion ต้องไม่ถูกเรียงใหม่ เพราะ PyYAML เรียงเป็นค่าตั้งต้นจึงต้องตั้ง sort_keys=False ต้องไม่มี anchor หรือ alias เกิดขึ้น โดย js-yaml ให้ตั้ง noRefs: true และตัวเลขยาวต้องไม่เสียหลัก ส่วนมานิเฟสต์ที่มีหลาย resource ให้ใช้ --split-exp แยกเป็นเอกสารที่คั่นด้วย ---
ทำไมเอาต์พุตของฉันเป็นบรรทัดเดียว {a: 1, b: 2} แทนที่จะมีย่อหน้า
นั่นคือรูปแบบ flow ซึ่งยังเป็น YAML ที่ถูกต้อง แต่เท่ากับไม่ได้แปลงเลย ใช้ -P บังคับรูปแบบบล็อก ถ้าใช้ PyYAML ให้เช็ก default_flow_style=False ซึ่งตั้งแต่ PyYAML 5.1 ค่าเริ่มต้นเปลี่ยนเป็น False แล้ว ส่วนเวอร์ชันเก่ากว่านั้นต้องเขียนระบุเอง
หมายเลขคำสั่งซื้อ 20 หลักเสียหลักท้ายไปแล้ว กู้คืนได้ไหม
กู้ไม่ได้ ทำได้แค่ป้องกัน yq แยกวิเคราะห์ผ่าน float64 จำนวนเต็มที่เกิน 2^53 จึงเสียความแม่นยำโดยตัวมันเอง ทางเลือกคือย้ายไปใช้ Python ซึ่งมีจำนวนเต็มความแม่นยำไม่จำกัดและตรวจแล้วว่าคงครบ หรือเก็บตัวเลขยาวเป็นสตริงใน JSON ตั้งแต่แรก ซึ่งยังแก้ปัญหาแบบเดียวกันของ JSON.parse ฝั่ง JavaScript ไปด้วย
เครื่องมือออนไลน์หรือบรรทัดคำสั่ง เลือกอะไรดี
คอนฟิกที่อ่อนไหวและภาพรวม API ที่มี token ให้ใช้บรรทัดคำสั่งหรือเครื่องมือที่รันฝั่งไคลเอนต์ ห้ามอัปโหลดเด็ดขาด งานครั้งเดียว ขนาดเล็ก ที่อยากกดดูภาพยืนยัน เครื่องมือออนไลน์จะเร็วกว่า หน้า /convert/yaml ของไซต์นี้รันอยู่ในเบราว์เซอร์ทั้งหมด ข้อมูลไม่เคยออกจากเครื่อง ส่วนงานที่เป็นชุด ต้องทำซ้ำได้ หรือผูกกับ CI ให้ใช้บรรทัดคำสั่งและเขียนขั้นตอนตรวจสอบลงในสคริปต์เดียวกันเลย