JSON ke YAML lewat baris perintah: empat jebakan di balik satu perintah yq
Intinya: Perintah yang paling sering disalin dari tutorial, yq -P input.json > output.yaml, pada yq v4 tetap mengeluarkan JSON. Artikel ini mengukur yq v4.44.6, PyYAML 6.0.3, dan js-yaml pada sampel 2.39 MB, menjelaskan mengapa ekstensi input menentukan format output, lalu memberi resep yang benar untuk konversi batch, tanda kutip, indentasi, dan presisi bilangan bulat besar.

Intinya: yq -P input.json > output.yaml — perintah yang paling sering disalin di tutorial — pada yq v4 tetap menghasilkan JSON. Untuk mendapatkan YAML Anda harus menuliskan -o=yaml (atau -oy) secara eksplisit. -P hanyalah kependekan dari --prettyPrint: ia mengatur indentasi dan gaya blok, bukan format output.
Mengonversi JSON ke YAML tidak pernah sesederhana mengganti ekstensi berkas. Biaya sebenarnya bersembunyi di empat tempat: ekstensi input menentukan format output, aturan tanda kutip diam-diam menulis ulang string Anda, indentasi sequence memengaruhi alat di hilir, dan bilangan bulat besar yang sekali lewat yq kehilangan presisi untuk selamanya.
1. Jalankan dulu perintah yang paling populer
Pada mesin bersih dengan yq v4.44.6 terpasang, buat sebuah input minimal:
printf '{"a":1,"b":{"c":2}}' > s.json
yq -P '.' s.json
Yang kembali bukan YAML, melainkan JSON berindentasi:
{
"a": 1,
"b": {
"c": 2
}
}
Jika Anda mengikuti tutorial dan mengalihkan perintah itu ke output.yaml, Anda akan mendapat berkas berekstensi .yaml berisi JSON. Setiap parser YAML 1.2 membacanya tanpa masalah (JSON adalah subset dari YAML 1.2; argumen lengkap tentang hubungan ini ada di batas konversi JSON, YAML, dan XML), jadi tidak ada yang langsung rusak. Baru rusak pada hari seseorang menambahkan komentar atau anchor, atau ketika yamllint di CI mulai memuntahkan sederet truthy value should be one of. Di repo bersama ini khususnya merepotkan: berkas seperti itu lolos review berbulan-bulan karena secara visual tampak seperti YAML biasa, dan kesalahan baru muncul saat struktur pertama kali diubah, ketika tidak ada lagi yang menyimpan JSON aslinya.
Perintah yang benar-benar menghasilkan YAML menyebut format outputnya secara eksplisit:
yq -o=yaml '.' s.json
# a: 1
# b:
# c: 2
2. Mengapa begini: ekstensi menentukan format output
yq v4 mengikuti aturan "format input menentukan format output". Ia menyimpulkan format dari ekstensi berkas input dan, tanpa -o, memakai lagi hasil tebakan itu. Input .json menghasilkan output JSON; input .yaml menghasilkan output YAML; membaca dari stdin tanpa ekstensi, default-nya YAML.
Satu input yang sama, empat cara penulisan, hasil pengukuran:
| Perintah | Sumber input | Format output sebenarnya | Hasil |
|---|---|---|---|
yq -P '.' s.json |
berkas s.json |
JSON (berindentasi) | jebakan saat dialihkan ke .yaml |
yq -o=yaml '.' s.json |
berkas s.json |
YAML (gaya blok) | benar |
yq -P -o=yaml '.' s.json |
berkas s.json |
YAML (gaya blok) | benar, -P di sini mubazir |
cat s.json | yq -P '.' |
stdin, tanpa ekstensi | YAML (gaya blok) | benar, karena default yaml |
Baris ketiga menunjukkan satu hal: -P tidak selalu perlu ketika -o=yaml sudah ada, tetapi ia juga tidak sepenuhnya aman — lihat data waktu di bagian keenam. Baris keempat menjelaskan mengapa "saya salin saja dan langsung jalan": banyak orang mengalirkan data lewat pipe, kebetulan melewati cabang yang memeriksa ekstensi. Jika Anda memindahkan perintah itu dari terminal interaktif ke skrip dengan nama berkas eksplisit, perilakunya berbalik. Karena itu selalu tuliskan -o=yaml secara eksplisit, meski hari ini data lewat pipe: hasilnya lalu tidak bergantung pada bagaimana input disambungkan pada proses berikutnya.
3. Konversi batch: tiga cara shell
Mengonversi satu berkas baru titik awal. Hampir semua alasan nyata melakukan ini di baris perintah bersifat batch: semua konfigurasi dalam satu direktori, semua snapshot API, diubah ke YAML sekaligus.
Loop bash / zsh (paling mudah dibaca, untuk satu direktori):
for f in configs/*.json; do
yq -o=yaml '.' "$f" > "${f%.json}.yaml"
done
find rekursif (kalau subdirektori juga perlu):
find . -name '*.json' -exec sh -c 'yq -o=yaml "." "$1" > "${1%.json}.yaml"' _ {} \;
PowerShell (di Windows CI):
Get-ChildItem *.json | ForEach-Object { yq -o=yaml '.' $_.FullName > ($_.BaseName + '.yaml') }
Ketiganya punya satu prasyarat keras: pastikan dulu JSON sumbernya valid. Konverter bukan parser: menghadapi kesalahan sintaks, banyak konverter hanya memberi posisi yang samar, bukan nomor baris yang jelas seperti validator. Menambah satu pemeriksaan di dalam loop hampir tidak berbiaya:
for f in configs/*.json; do
jq empty "$f" || { echo "invalid: $f"; continue; }
yq -o=yaml '.' "$f" > "${f%.json}.yaml"
done
jq empty hanya mem-parse tanpa mencetak apa pun; JSON tidak valid mengembalikan exit code bukan nol. Dipadukan dengan set -euo pipefail, seluruh pipeline berhenti di berkas rusak pertama alih-alih menghasilkan setumpuk keluaran setengah jadi. Untuk build malam yang memproses ratusan konfigurasi, ini bedanya antara satu kesalahan yang jelas di log dan puluhan YAML rusak yang harus dibersihkan manual. Ada baiknya juga memutuskan lebih dulu apa yang terjadi pada berkas .yaml yang sudah ada: loop akan menimpanya tanpa peringatan, jadi di repo yang mencampur dua format, tambahkan pemeriksaan sebelum mengalihkan output.
4. Empat parameter output, empat tugas terpisah
Daftar parameter yq panjang, tetapi di jalur JSON → YAML hanya empat yang perlu diingat:
| Parameter | Fungsi | Kapan wajib ditulis |
|---|---|---|
-o=yaml / -oy |
menetapkan format output ke YAML | saat input berupa berkas .json |
-P / --prettyPrint |
memaksa gaya blok dan menormalkan indentasi | membaca dari stdin, atau saat indentasi sequence penting |
-I=n |
lebar indentasi, default 2 | pihak hilir minta 4 spasi (sebagian panduan gaya Ansible) |
--split-exp |
memecah elemen array menjadi dokumen terpisah | saat membuat manifest Kubernetes multi-resource |
--split-exp layak dibahas tersendiri. Manifest Kubernetes lazim memisahkan resource dengan ---. Membentuknya dari array JSON:
yq -P -o=yaml '.[] | splitDoc' --split-exp 'true' kubernetes-list.json
Tanpa --split-exp Anda mendapat satu sequence YAML; dengannya, setiap elemen array menjadi dokumen tersendiri yang dipisahkan ---. kubectl apply menerima keduanya, tetapi keterbacaan diff berbeda satu orde besarnya. Di tim yang meninjau manifest dengan mata sebelum deploy, justru perbedaan inilah yang menentukan apakah seseorang sempat melihat field tambahan di resources sebelum ia masuk ke cluster.
5. Tanda kutip: enam bentuk string yang wajib dikutip
Inferensi tipe di YAML bersifat implisit: nilai string yang tampak seperti angka atau boolean, tanpa tanda kutip, akan dibaca sebagai tipe lain. Ini bukan risiko teoretis — diukur pada yq v4.44.6, keenam nilai di bawah otomatis diberi tanda kutip, artinya parser memang akan salah membacanya:
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"
Akibatnya, satu per satu:
| Nilai asli (string JSON) | Tanpa kutip terbaca sebagai | Dampak nyata |
|---|---|---|
"1.0" |
float 1.0 |
nomor versi jadi desimal, 1.10 menyusut jadi 1.1 |
"true" |
boolean true |
logika percabangan terbalik atau tipe tidak cocok |
"8080" |
integer 8080 |
tipe port berubah, perbandingan string gagal |
"" |
null |
string kosong hilang, pihak hilir menerima None |
"host: value" |
error parsing atau mapping bersarang | setelah titik dua wajib ada spasi |
"2026-11-03" |
objek tanggal | dideserialisasi jadi datetime.date, bukan string |
Baris terakhir paling licik. Di JSON tanggal hanyalah string; setelah dikonversi ia menjadi tipe tanggal YAML, dan saat dikonversi balik banyak implementasi menuliskannya sebagai objek tanggal berformat ISO, bukan string asal, sehingga konsistensi round-trip putus. Baik PyYAML maupun yq memilih menambahkan tanda kutip pada nilai-nilai ini, yang berarti keduanya mengakui risikonya. Kasus ini paling sering muncul pada konfigurasi dengan field seperti release_date atau valid_until: tipenya berubah diam-diam, tanpa satu pun peringatan di log build, dan ketahuan hanya di kode aplikasi yang mengharapkan string.
Catatan tambahan, baris unicode: 端口 8080 justru keluar tanpa tanda kutip — teks CJK aman di YAML. Yang perlu dikhawatirkan bukan itu, melainkan urutan kunci dan presisi angka yang dibahas di bagian berikutnya.
6. Padanan Python dan Node beserta waktu terukur
yq tidak bisa dipasang di semua mesin: lingkungan tanpa jaringan, image CI yang dikunci, Windows tanpa package manager. Padanan Python dan Node tersedia, masing-masing dengan jebakan default-nya sendiri. Tabel perbandingan lengkap ketiga jalur berdasarkan skenario ada di pemilihan alat baris perintah JSON dan editor; artikel ini hanya fokus pada satu arah JSON → YAML.
Python + PyYAML (tiga argumen, tak ada yang boleh tertinggal):
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
)
Ketiganya berdefault False / False / True, artinya PyYAML sebagaimana adanya akan mengurutkan kunci secara alfabetis dan mengubah teks CJK menjadi \uXXXX. Diukur pada input yang sama dengan parameter default, hasilnya:
apiVersion: v1
data:
LOG_LEVEL: info
colon: 'host: value'
unicode: "\u7AEF\u53E3 8080"
kind: ConfigMap
metadata:
labels:
app: web
name: app-config
Perhatikan kind kini berada setelah apiVersion namun sebelum metadata — dalam manifest Kubernetes, apiVersion semestinya mendahului kind sesuai konvensi komunitas; urutan kunci yang berantakan tidak merusak parsing, tetapi membuat review menyiksa. Di tim yang membandingkan manifest dengan mata pada setiap pull request, ini cepat berubah menjadi sumber diff yang tidak bermakna: hanya baris berisi kunci yang bertukar tempat, sementara isi konfigurasi tetap sama.
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 mematikan pembungkusan baris panjang (kalau tidak, URL panjang terpotong di tengah), sedangkan noRefs: true mencegah pembentukan anchor dan alias — wajib saat konsumennya adalah kubectl atau alat lain yang tidak mengenal anchor. Keduanya sekelompok dengan sortKeys: false: ketiganya harus ditulis eksplisit karena pustaka dioptimalkan untuk output ringkas, bukan untuk konfigurasi yang dibaca manusia.
Waktu terukur (sampel: JSON 2.39 MB, 12000 record, masing-masing berisi objek bersarang dan dua array; mesin adalah sandbox yang dipakai untuk tugas ini):
| Implementasi | Konversi | Parse balik | Round-trip |
|---|---|---|---|
yq -o=yaml v4.44.6 |
0.45 s | 0.45 s | identik |
yq -P -o=yaml v4.44.6 |
4.16 s | — | identik |
| Node js-yaml 4.x | 0.23 s | 0.11 s | identik |
| Python PyYAML 6.0.3 | 3.26 s | 8.01 s | identik |
Dua hasil yang berlawanan dengan intuisi. Pertama, menambahkan -P membuat yq sekitar 9 kali lebih lambat (0.45 s → 4.16 s), padahal pada sampel ini jumlah byte output identik (2,326,895 byte untuk kedua arah) — -P melakukan satu putaran penataan ulang tata letak penuh, dan biayanya terasa saat mengonversi ratusan berkas. Kedua, parse balik PyYAML (8.01 s) lebih dari dua kali lebih lambat daripada dump maju (3.26 s), jadi Python bukan pilihan bagus kalau pipeline Anda berulang kali membaca kembali YAML.
Satu lagi: 2.39 MB JSON setelah menjadi YAML gaya blok tinggal 2.33 MB, justru sekitar 3% lebih kecil. Mitos yang beredar bahwa "YAML selalu lebih besar dari JSON" tidak benar — pada nested yang dalam, tanda kutip, koma, dan kurung kurawal JSON bisa lebih berat daripada spasi indentasi YAML.
7. Tiga hal yang tidak bisa kembali
Bagian-bagian sebelumnya membahas "cara mengonversi dengan benar"; bagian ini membahas "apa yang hilang". Diukur dengan input yang sengaja dirancang:
printf '{"order_id":12345678901234567890,"zip":"0150","ts":"2026-11-03","f":1.0e6}' > t.json
yq -o=yaml '.' t.json
yq mengeluarkan:
order_id: 1.2345678901234567e+19
zip: "0150"
ts: "2026-11-03"
f: 1000000
Lalu dikonversi balik ke JSON: {"order_id":12345678901234567000,"zip":"0150","ts":"2026-11-03","f":1000000}.
Nomor order 20 digit 12345678901234567890 berubah menjadi 12345678901234567000 — tiga digit terakhir menjadi nol, tanpa peringatan sama sekali. Penyebabnya: yq memakai jalur parse float64 dari Go; bilangan bulat di atas 2^53 tidak bisa direpresentasikan tepat, sehingga output beralih ke notasi ilmiah dan nilai aslinya sudah hilang saat dikonversi balik.
Input yang sama lewat PyYAML:
order_id: 12345678901234567890
zip: '0150'
ts: '2026-11-03'
f: 1000000.0
Bilangan bulat di Python berpresisi arbitrer, jadi 20 digit itu utuh dan saat dibaca balik tetap 12345678901234567890. Kalau data Anda memuat nomor order, snowflake ID, atau nomor identitas panjang, jangan pakai yq — pakai Python, atau ubah field itu menjadi string sejak di JSON. Mekanisme lengkap di balik kelas kegagalan ini dijelaskan di mengapa bilangan bulat besar kehilangan presisi di JSON.
Hal kedua yang hilang adalah pembeda float dan int: 1.0e6 pada output yq menjadi 1000000 (integer), sementara PyYAML mempertahankan 1000000.0. Ketiga adalah urutan kunci — PyYAML mengurutkan secara default, yq dan js-yaml mempertahankan secara default, dan ini sumber perbedaan tersembunyi di tim lintas bahasa. Ketiganya punya kesamaan: perintah selesai dengan exit code 0, jadi kehilangan hanya terdeteksi dengan membandingkan input dan output secara langsung, bukan dari status proses.
8. Di CI jangan cuma convert: validasi → konversi → lint
Menganggap konversi sebagai satu-satunya langkah adalah cacat pipeline paling umum. Exit code 0 hanya berarti "JSON input berhasil di-parse", bukan "output sudah benar" dan bukan pula "output memenuhi syarat konsumennya". Bentuk tiga tahap yang benar:
#!/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
Baris yq -o=json ... > /dev/null di langkah 3 terlihat mubazir, padahal justru paling berguna di seluruh blok: ia memaksa hasil akhir di-parse ulang. Salah indentasi, anchor yang gagal diresolusi, dan tipe yang ditulis ulang akan muncul di sini. Lewati baris itu, masalahnya baru meledak di kubectl apply atau saat aplikasi mulai, ketika harga kesalahannya sudah jauh berbeda.
Kalau hasilnya berupa konfigurasi yang dirawat manusia (Helm values, docker-compose, workflow GitHub Actions), periksa tiga hal lagi: apakah urutan kunci berantakan, apakah angka panjang kehilangan digit, dan apakah komentar perlu ditambahkan manual. JSON tidak punya komentar, jadi semua catatan harus ditulis setelah konversi; tidak ada konverter yang akan membuatnya untuk Anda. Kebiasaan yang baik adalah menyimpan catatan singkat di sebelah berkas hasil tentang field mana yang perlu dicek manual, agar anggota tim berikutnya tidak mengira YAML hasil generate itu sumber kebenaran final.
FAQ
yq -P atau -o=yaml, yang mana yang harus dipakai?
-o=yaml menentukan "apakah output itu YAML", sedangkan -P menentukan "seperti apa YAML itu tampil". Untuk input berkas .json, -o=yaml wajib, dan -P hanya ditambahkan saat Anda ingin memaksa gaya blok atau indentasi sequence. Membaca dari stdin tanpa ekstensi, yq sudah default ke YAML, jadi -P saja cukup.
YAML hasil konversi bisa langsung dipakai Kubernetes?
Secara sintaks bisa, dengan tiga syarat: kunci yang punya konvensi urutan seperti apiVersion tidak diurut ulang (PyYAML mengurutkan secara default, set sort_keys=False), tidak ada anchor dan alias yang dihasilkan (js-yaml: set noRefs: true), dan angka panjang tidak kehilangan digit. Untuk manifest multi-resource, pecah juga menjadi dokumen berpemisah --- dengan --split-exp.
Kenapa output saya cuma satu baris {a: 1, b: 2}, bukan berindentasi?
Itu flow style. Masih valid, tetapi sama saja dengan tidak mengonversi. Pakai -P untuk memaksa gaya blok; kalau memakai PyYAML, pastikan default_flow_style=False (sejak PyYAML 5.1 nilainya sudah False, versi lama perlu menuliskannya eksplisit).
Nomor order 20 digit kehilangan digit terakhir, bisa dipulihkan?
Tidak bisa, hanya bisa dicegah. yq mem-parse lewat float64, jadi bilangan bulat di atas 2^53 pasti kehilangan presisi. Entah beralih ke Python (bilangan bulat presisi arbitrer, terverifikasi tanpa kehilangan), atau simpan angka panjang sebagai string sejak di JSON — pilihan kedua lebih aman karena sekaligus menyelesaikan masalah serupa pada JSON.parse di sisi JavaScript.
Pilih konverter online atau baris perintah?
Untuk konfigurasi berisi rahasia dan snapshot API yang memuat token, pakai baris perintah lokal atau alat murni sisi klien, jangan pernah mengunggah. Untuk kebutuhan sekali pakai, ukuran kecil, dan ingin konfirmasi visual, alat online lebih cepat; halaman /convert/yaml di situs ini berjalan sepenuhnya di browser dan data tidak pernah meninggalkan mesin Anda. Untuk semua yang bersifat batch, masuk CI, atau harus dapat direproduksi, pakai baris perintah dan tuliskan langkah validasi ke dalam skrip yang sama.