JSON sang YAML bằng dòng lệnh: bốn cái bẫy đằng sau một lệnh yq
Kết luận: Lệnh bị chép lại nhiều nhất trong mọi tutorial, yq -P input.json > output.yaml, trên yq v4 vẫn xuất ra JSON. Bài này đo yq v4.44.6, PyYAML 6.0.3 và js-yaml trên mẫu 2.39 MB, giải thích vì sao phần mở rộng đầu vào quyết định định dạng đầu ra, và đưa ra cách viết đúng cho chuyển đổi hàng loạt, dấu ngoặc, thụt lề và độ chính xác số nguyên lớn.

Kết luận: yq -P input.json > output.yaml — lệnh bị chép lại nhiều nhất trong các tutorial — trên yq v4 vẫn xuất ra JSON. Muốn có YAML phải ghi rõ -o=yaml (hoặc -oy). -P chỉ là viết tắt của --prettyPrint: nó điều khiển thụt lề và kiểu khối, không điều khiển định dạng đầu ra.
Chuyển JSON sang YAML chưa bao giờ đơn giản là đổi phần mở rộng tệp. Cái giá thật sự nằm ở bốn chỗ: phần mở rộng của đầu vào quyết định định dạng đầu ra, quy tắc dấu ngoặc âm thầm viết lại chuỗi của bạn, thụt lề của sequence ảnh hưởng tới công cụ downstream, và một số nguyên lớn đi qua yq sẽ mất độ chính xác vĩnh viễn.
1. Chạy thử lệnh phổ biến nhất trước đã
Trên một máy sạch đã cài yq v4.44.6, hãy tạo một đầu vào tối thiểu:
printf '{"a":1,"b":{"c":2}}' > s.json
yq -P '.' s.json
Thứ nhận lại không phải YAML, mà là JSON có thụt lề:
{
"a": 1,
"b": {
"c": 2
}
}
Nếu bạn làm theo tutorial và chuyển hướng lệnh đó vào output.yaml, bạn sẽ có một tệp mang đuôi .yaml nhưng nội dung là JSON. Mọi parser YAML 1.2 đều đọc được bình thường (JSON là tập con của YAML 1.2; lập luận đầy đủ về quan hệ chứa này nằm ở ranh giới chuyển đổi JSON, YAML và XML), nên một thời gian dài sẽ không có lỗi nào — cho tới ngày ai đó thêm comment hoặc anchor, hoặc yamllint trong CI bắt đầu phun ra một đống truthy value should be one of. Trong các repo dùng chung, kiểu tệp này đặc biệt khó chịu: nó trôi qua review hàng tháng trời vì nhìn bằng mắt vẫn như YAML bình thường, và lỗi chỉ lộ ra ở lần đầu tiên cấu trúc bị sửa, lúc đó không còn ai giữ bản JSON gốc nữa.
Lệnh thật sự sinh ra YAML là lệnh ghi rõ định dạng đầu ra:
yq -o=yaml '.' s.json
# a: 1
# b:
# c: 2
2. Vì sao lại thế: phần mở rộng quyết định định dạng đầu ra
yq v4 hoạt động theo nguyên tắc "định dạng đầu vào quyết định định dạng đầu ra". Nó suy ra định dạng từ phần mở rộng của tệp đầu vào và khi không có -o thì dùng lại kết quả suy luận đó. Đầu vào .json cho đầu ra JSON; đầu vào .yaml cho đầu ra YAML; đọc từ stdin mà không có phần mở rộng thì mặc định là YAML.
Cùng một đầu vào, bốn cách viết, kết quả đo thực tế:
| Lệnh | Nguồn đầu vào | Định dạng đầu ra thực tế | Kết luận |
|---|---|---|---|
yq -P '.' s.json |
tệp s.json |
JSON (có thụt lề) | bẫy khi chuyển hướng sang .yaml |
yq -o=yaml '.' s.json |
tệp s.json |
YAML (kiểu khối) | đúng |
yq -P -o=yaml '.' s.json |
tệp s.json |
YAML (kiểu khối) | đúng, -P ở đây thừa |
cat s.json | yq -P '.' |
stdin, không có phần mở rộng | YAML (kiểu khối) | đúng, vì mặc định là yaml |
Dòng thứ ba cho thấy một điều: -P không phải lúc nào cũng cần khi đã có -o=yaml, nhưng nó cũng không vô hại — xem số liệu ở mục sáu. Dòng thứ tư giải thích vì sao "chép nguyên lệnh mà vẫn chạy": nhiều người đẩy dữ liệu qua pipe, tình cờ né đúng nhánh xử lý phần mở rộng. Nếu mang lệnh đó từ terminal tương tác sang một script có tên tệp rõ ràng, hành vi sẽ đảo ngược. Vì vậy hãy luôn ghi -o=yaml tường minh, kể cả khi hôm nay dữ liệu đi qua pipe: kết quả khi đó không còn phụ thuộc vào cách ai đó nối đầu vào ở lần chạy sau.
3. Chuyển đổi hàng loạt: ba cách viết shell
Chuyển một tệp chỉ là khởi đầu. Hầu như mọi lý do thực tế để làm việc này trên dòng lệnh đều là hàng loạt: mọi file cấu hình trong một thư mục, mọi bản snapshot API, đổi sang YAML một lượt.
Vòng lặp bash / zsh (dễ đọc nhất, hợp với một thư mục):
for f in configs/*.json; do
yq -o=yaml '.' "$f" > "${f%.json}.yaml"
done
find đệ quy (khi cần cả thư mục con):
find . -name '*.json' -exec sh -c 'yq -o=yaml "." "$1" > "${1%.json}.yaml"' _ {} \;
PowerShell (trên Windows CI):
Get-ChildItem *.json | ForEach-Object { yq -o=yaml '.' $_.FullName > ($_.BaseName + '.yaml') }
Cả ba cách có chung một điều kiện cứng: phải xác nhận JSON nguồn hợp lệ trước đã. Converter khác validator: gặp lỗi cú pháp, nhiều converter chỉ đưa ra một vị trí mơ hồ thay vì chỉ rõ số dòng như validator. Thêm một bước kiểm tra vào vòng lặp hầu như không tốn chi phí:
for f in configs/*.json; do
jq empty "$f" || { echo "invalid: $f"; continue; }
yq -o=yaml '.' "$f" > "${f%.json}.yaml"
done
jq empty chỉ phân tích chứ không in ra gì; JSON không hợp lệ sẽ trả mã thoát khác không. Kết hợp với set -euo pipefail, cả pipeline dừng ngay ở tệp hỏng đầu tiên thay vì tạo ra một đống sản phẩm chuyển dở. Với các bản build chạy đêm xử lý hàng trăm file cấu hình, đó là khác biệt giữa một lỗi rõ ràng trong log và hàng chục file YAML hỏng phải dọn bằng tay. Cũng nên quyết định sẵn chuyện gì xảy ra với các tệp .yaml đã tồn tại: vòng lặp sẽ ghi đè chúng mà không báo trước, nên trong repo lẫn lộn hai định dạng, hãy thêm một bước kiểm tra trước khi chuyển hướng đầu ra.
4. Bốn tham số đầu ra, bốn việc riêng biệt
Bảng tham số của yq rất dài, nhưng trên đường JSON → YAML chỉ cần nhớ bốn cái:
| Tham số | Tác dụng | Khi nào bắt buộc phải ghi |
|---|---|---|
-o=yaml / -oy |
chỉ định định dạng đầu ra là YAML | khi đầu vào là tệp .json |
-P / --prettyPrint |
ép kiểu khối và chuẩn hóa thụt lề | khi đọc từ stdin, hoặc khi cần thụt lề sequence |
-I=n |
số dấu cách thụt lề, mặc định 2 | downstream yêu cầu 4 dấu cách (một số quy chuẩn Ansible) |
--split-exp |
tách phần tử mảng thành nhiều tài liệu | khi sinh manifest Kubernetes nhiều resource |
Cái --split-exp đáng nói riêng. Manifest Kubernetes thường phân tách các resource bằng ---. Sinh dạng đó từ một mảng JSON:
yq -P -o=yaml '.[] | splitDoc' --split-exp 'true' kubernetes-list.json
Không có --split-exp bạn được một sequence YAML; có nó, mỗi phần tử mảng trở thành một tài liệu riêng biệt ngăn bằng ---. Cả hai tệp kubectl apply đều nuốt được, nhưng độ dễ đọc của diff khác nhau cả một bậc. Ở những đội review manifest bằng mắt trước khi triển khai, chính khác biệt này quyết định việc ai đó có phát hiện ra một trường thừa trong resources trước khi nó lọt vào cluster hay không.
5. Dấu ngoặc: sáu dạng chuỗi bắt buộc phải đặt trong ngoặc
YAML suy luận kiểu ngầm định: một giá trị chuỗi trông giống số hoặc boolean, nếu không có dấu ngoặc, sẽ bị đọc thành kiểu khác. Đây không phải rủi ro lý thuyết — đo trên yq v4.44.6, cả sáu giá trị dưới đây đều tự động được thêm dấu ngoặc, chứng tỏ parser quả thật sẽ đọc nhầm chúng:
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"
Hệ quả, từng cái một:
| Giá trị gốc (chuỗi JSON) | Nếu không có ngoặc sẽ đọc thành | Tác động thực tế |
|---|---|---|
"1.0" |
số thực 1.0 |
số phiên bản thành số thập phân, 1.10 thành 1.1 |
"true" |
boolean true |
logic rẽ nhánh đảo ngược hoặc lệch kiểu |
"8080" |
số nguyên 8080 |
kiểu của port đổi, so sánh chuỗi thất bại |
"" |
null |
chuỗi rỗng biến mất, downstream nhận None |
"host: value" |
lỗi phân tích hoặc mapping lồng nhau | sau dấu hai chấm bắt buộc phải có dấu cách |
"2026-11-03" |
đối tượng ngày | giải tuần tự thành datetime.date thay vì chuỗi |
Dòng cuối là cái hiểm nhất. Trong JSON, ngày chỉ là chuỗi; sau khi chuyển nó thành kiểu ngày của YAML, và khi chuyển ngược lại nhiều implementation sẽ xuất ra đối tượng ngày theo định dạng ISO thay vì chuỗi gốc — tính nhất quán khứ hồi gãy ngay tại đó. Cả PyYAML lẫn yq đều chọn thêm dấu ngoặc cho những giá trị này, tức là đã thừa nhận điều đó. Lỗi này thường lộ ra ở các file cấu hình có trường kiểu release_date hay valid_until: kiểu đổi im lặng, không một dòng cảnh báo trong log build, và chỉ bị bắt ở tầng code ứng dụng vốn đang trông đợi một chuỗi.
Nhân tiện, dòng unicode: 端口 8080 không bị thêm dấu ngoặc — chữ Hán trong YAML an toàn. Thứ cần lo không phải nó, mà là thứ tự khóa và độ chính xác số, nói ở các mục sau.
6. Cách viết tương đương bằng Python và Node kèm số liệu thời gian
Không phải máy nào cũng cài được yq (môi trường offline, image CI bị giới hạn, Windows không có package manager). Cách viết tương đương bằng Python và Node đều có cái bẫy mặc định riêng. Bảng đối chiếu đầy đủ cho ba lộ trình theo từng tình huống nằm ở lựa chọn công cụ dòng lệnh JSON và editor; bài này chỉ tập trung vào một chiều JSON → YAML.
Python + PyYAML (ba tham số, thiếu một cái là hỏng):
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
)
Ba tham số này mặc định là False / False / True, tức là PyYAML dùng ngay sau khi cài sẽ sắp xếp lại khóa theo alphabet và biến chữ Hán thành \uXXXX. Đo trên cùng một đầu vào với tham số mặc định, đầu ra là:
apiVersion: v1
data:
LOG_LEVEL: info
colon: 'host: value'
unicode: "\u7AEF\u53E3 8080"
kind: ConfigMap
metadata:
labels:
app: web
name: app-config
Để ý kind bị đẩy xuống sau apiVersion nhưng đứng trước metadata — trong manifest Kubernetes, cộng đồng quy ước apiVersion phải đứng trước kind; thứ tự khóa bị xáo trộn không ảnh hưởng việc phân tích, nhưng làm người review rất khó chịu. Ở những đội so sánh manifest bằng mắt trên mọi pull request, điều này nhanh chóng biến thành nguồn diff vô nghĩa: chỉ các dòng chứa khóa đổi chỗ, còn nội dung cấu hình vẫn nguyên.
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 tắt việc bẻ dòng dài (nếu không, URL dài sẽ bị cắt ngang giữa chừng), còn noRefs: true cấm sinh anchor và alias — bắt buộc phải tắt khi downstream là kubectl hoặc công cụ không nhận anchor. Cả hai tùy chọn cùng chung nhóm với sortKeys: false: cả ba đều phải ghi tường minh, vì thư viện được tối ưu cho đầu ra gọn, chứ không phải cho cấu hình để con người đọc.
Thời gian đo thực tế (mẫu: JSON 2.39 MB, 12000 bản ghi, mỗi bản gồm một object lồng nhau và hai mảng; máy là sandbox dùng cho chính tác vụ này):
| Implementation | Thời gian chuyển đổi | Thời gian phân tích ngược | Khứ hồi |
|---|---|---|---|
yq -o=yaml v4.44.6 |
0.45 s | 0.45 s | giống hệt |
yq -P -o=yaml v4.44.6 |
4.16 s | — | giống hệt |
| Node js-yaml 4.x | 0.23 s | 0.11 s | giống hệt |
| Python PyYAML 6.0.3 | 3.26 s | 8.01 s | giống hệt |
Hai điểm ngược trực giác. Thứ nhất, thêm -P làm yq chậm đi khoảng 9 lần (0.45 s → 4.16 s), trong khi trên mẫu này số byte đầu ra hoàn toàn giống hệt (2,326,895 byte ở cả hai chiều) — -P thực hiện một lượt dàn lại toàn bộ bố cục, và khi chuyển hàng trăm tệp thì chi phí đó cộng dồn đáng kể. Thứ hai, phân tích ngược của PyYAML (8.01 s) chậm hơn gấp đôi so với chiều xuôi (3.26 s), nên Python không phải lựa chọn tốt nếu pipeline của bạn phải đọc lại YAML nhiều lần.
Thêm một chi tiết: 2.39 MB JSON sau khi chuyển sang YAML kiểu khối còn 2.33 MB, tức nhỏ đi khoảng 3%. Câu truyền miệng "YAML luôn lớn hơn JSON" không đúng — khi lồng sâu, dấu ngoặc, dấu phẩy và dấu ngoặc nhọn của JSON có thể chiếm nhiều chỗ hơn phần khoảng trắng thụt lề của YAML.
7. Ba thứ chuyển đi rồi không lấy lại được
Các mục trước nói "làm sao chuyển cho đúng", mục này nói "cái gì bị mất sau khi chuyển". Dùng một đầu vào được dựng riêng để đo:
printf '{"order_id":12345678901234567890,"zip":"0150","ts":"2026-11-03","f":1.0e6}' > t.json
yq -o=yaml '.' t.json
yq xuất ra:
order_id: 1.2345678901234567e+19
zip: "0150"
ts: "2026-11-03"
f: 1000000
Rồi chuyển ngược về JSON: {"order_id":12345678901234567000,"zip":"0150","ts":"2026-11-03","f":1000000}.
Số đơn hàng 20 chữ số 12345678901234567890 đã biến thành 12345678901234567000 — ba chữ số cuối về không, và không hề có cảnh báo nào. Nguyên nhân: yq đi theo đường phân tích float64 của Go; số nguyên vượt quá 2^53 không thể biểu diễn chính xác, nên đầu ra chuyển sang ký pháp khoa học, và khi chuyển ngược thì giá trị gốc đã mất hẳn.
Cùng đầu vào đó đưa cho PyYAML:
order_id: 12345678901234567890
zip: '0150'
ts: '2026-11-03'
f: 1000000.0
Số nguyên trong Python có độ chính xác tùy ý, nên cả 20 chữ số được giữ nguyên và đọc ngược lại vẫn là 12345678901234567890. Nếu dữ liệu của bạn có số đơn hàng, snowflake ID hay số căn cước dạng số dài, đừng dùng yq — hãy dùng Python, hoặc đổi trường đó thành chuỗi ngay từ trong JSON. Cơ chế đầy đủ của nhóm lỗi mất độ chính xác này được giải thích trong vì sao số nguyên lớn trong JSON mất độ chính xác.
Thứ hai bị mất là sự phân biệt float và int: 1.0e6 trong đầu ra yq thành 1000000 (số nguyên), còn PyYAML giữ 1000000.0. Thứ ba là thứ tự khóa — PyYAML mặc định sắp xếp lại, yq và js-yaml mặc định giữ nguyên; trong đội dùng nhiều ngôn ngữ, đó là nguồn bất đồng giấu mặt. Cả ba điểm chung một chuyện: lệnh kết thúc với mã thoát 0, nên chỉ so sánh trực tiếp đầu vào với đầu ra mới phát hiện được mất mát, còn trạng thái chạy thì không cho biết gì.
8. Trong CI đừng chỉ convert: kiểm tra → chuyển đổi → lint
Coi chuyển đổi là bước duy nhất là lỗi pipeline phổ biến nhất. Mã thoát 0 chỉ nói "JSON đầu vào phân tích được", không nói "đầu ra đúng" hay "đầu ra đáp ứng yêu cầu của downstream". Dạng đúng gồm ba đoạn:
#!/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
Dòng yq -o=json ... > /dev/null ở bước 3 nhìn thì thừa, thực tế lại là dòng hữu ích nhất cả khối: nó bắt buộc phân tích lại sản phẩm thêm một lượt. Lỗi thụt lề, anchor không giải được, kiểu bị viết lại — tất cả lộ ra ngay tại đây. Bỏ qua nó, vấn đề sẽ dời sang lúc kubectl apply hoặc lúc ứng dụng khởi động, nơi cái giá của lỗi hoàn toàn khác.
Nếu sản phẩm là cấu hình do con người duy trì (Helm values, docker-compose, workflow GitHub Actions), hãy kiểm tra thêm ba việc: thứ tự khóa có bị xáo trộn không, số dài có mất chữ số không, và comment có cần tay bổ sung không. JSON không có comment, nên mọi chú thích chỉ có thể thêm sau khi chuyển đổi; không converter nào tự sinh ra chúng cho bạn. Một thói quen tốt là để cạnh file sinh ra một ghi chú ngắn về những trường cần kiểm tra thủ công, để người tiếp theo trong đội không nhầm YAML tự động sinh thành nguồn sự thật cuối cùng.
FAQ
Nên dùng yq -P hay -o=yaml?
-o=yaml quyết định "đầu ra có phải YAML không", còn -P quyết định "YAML trông như thế nào". Với đầu vào là tệp .json thì -o=yaml là bắt buộc, còn -P chỉ thêm khi bạn cần ép kiểu khối hoặc thụt lề sequence. Đọc từ stdin không có phần mở rộng thì yq mặc định đã xuất YAML, khi đó chỉ dùng -P là đủ.
YAML sau khi chuyển có đưa thẳng vào Kubernetes được không?
Về cú pháp thì được, nhưng có ba điều kiện: các khóa có quy ước thứ tự như apiVersion không bị sắp xếp lại (PyYAML mặc định sắp xếp, phải đặt sort_keys=False), không sinh ra anchor và alias (js-yaml: đặt noRefs: true), và số dài không mất chữ số. Với manifest nhiều resource, còn phải dùng --split-exp để tách thành các tài liệu ngăn bằng ---.
Sao đầu ra của tôi là một dòng {a: 1, b: 2}, không có thụt lề?
Đó là flow style. Nó vẫn hợp lệ, nhưng coi như chưa chuyển. Dùng -P để ép kiểu khối; nếu dùng PyYAML, kiểm tra default_flow_style=False (từ PyYAML 5.1 giá trị mặc định đã là False, bản cũ hơn phải ghi tường minh).
Số đơn hàng 20 chữ số bị mất mấy chữ cuối, có cứu được không?
Không cứu được, chỉ có thể phòng. yq phân tích qua float64, nên số nguyên vượt 2^53 chắc chắn mất độ chính xác. Hoặc chuyển sang Python (số nguyên độ chính xác tùy ý, đã kiểm chứng giữ nguyên), hoặc lưu số dài dưới dạng chuỗi ngay trong JSON — cách sau chắc chắn hơn, vì nó đồng thời giải quyết luôn lỗi tương tự của JSON.parse phía JavaScript.
Chọn công cụ online hay dòng lệnh?
Với cấu hình chứa bí mật và bản snapshot API có token, dùng dòng lệnh trên máy hoặc công cụ thuần front-end, tuyệt đối không upload. Với dữ liệu nhỏ, dùng một lần, cần nhìn trực quan để xác nhận thì công cụ online nhanh hơn; trang /convert/yaml trên site này chạy hoàn toàn trong trình duyệt, dữ liệu không rời khỏi máy. Với mọi thứ hàng loạt, cần vào CI, cần tái lập được thì dùng dòng lệnh và ghi luôn các bước kiểm tra vào cùng một script.