Konwersja JSON do YAML w wierszu poleceń: cztery pułapki za jedną komendą yq
Podsumowanie: Najczęściej kopiowana w poradnikach komenda yq -P input.json > output.yaml wciąż produkuje JSON. W artykule yq v4.44.6, PyYAML 6.0.3 i js-yaml przetestowano na próbce 2.39 MB, wyjaśniono, dlaczego format wyjścia zależy od rozszerzenia wejścia, i podano poprawne sposoby konwersji wsadowej, cudzysłowów, wcięć oraz precyzji dużych liczb całkowitych.

Wniosek: yq -P input.json > output.yaml — komenda najczęściej kopiowana z poradników — na yq v4 nadal daje JSON. Aby otrzymać YAML, trzeba jawnie podać -o=yaml (albo -oy). -P to skrót od --prettyPrint: odpowiada za wcięcia i styl blokowy, a nie za format wyjścia.
Konwersja JSON do YAML nigdy nie polegała na „zmianie rozszerzenia pliku”. Prawdziwy koszt kryje się w czterech miejscach: format wyjścia zależy od rozszerzenia wejścia, reguły cudzysłowów po cichu przepisują twoje łańcuchy, wcięcia sekwencji wpływają na narzędzia downstream, a duża liczba całkowita, która przejdzie przez yq, traci precyzję na zawsze.
1. Uruchom najpierw tę najpopularniejszą komendę
Na czystej maszynie z zainstalowanym yq v4.44.6 utwórz minimalne wejście:
printf '{"a":1,"b":{"c":2}}' > s.json
yq -P '.' s.json
Wracają nie YAML, lecz JSON z wcięciami:
{
"a": 1,
"b": {
"c": 2
}
}
Jeśli zgodnie z poradnikiem przekierujesz to do output.yaml, dostaniesz plik z rozszerzeniem .yaml, a w środku JSON. Każdy parser YAML 1.2 przeczyta go bez problemu (w YAML 1.2 JSON jest podzbiorem — pełne wyprowadzenie tej relacji zawierania znajdziesz w artykule o granicach konwersji JSON ↔ YAML i XML), więc nic nie psuje się od razu. Psuje się w dniu, w którym ktoś doda komentarz lub kotwicę, albo gdy yamllint w CI zacznie sypać setkami komunikatów truthy value should be one of.
Wywołanie, które naprawdę produkuje YAML, nazywa format wyjścia wprost:
yq -o=yaml '.' s.json
# a: 1
# b:
# c: 2
2. Dlaczego tak się dzieje: rozszerzenie decyduje o formacie wyjścia
yq v4 działa według reguły „format wejścia decyduje o formacie wyjścia”. Format jest wnioskowany z rozszerzenia pliku wejściowego, a bez -o ten wniosek jest użyty ponownie. Wejście .json → wyjście JSON. Wejście .yaml → wyjście YAML. Odczyt ze stdin bez rozszerzenia → domyślnie YAML.
To samo wejście, cztery sposoby wywołania, wyniki zmierzone:
| Komenda | Źródło wejścia | Faktyczny format wyjścia | Werdykt |
|---|---|---|---|
yq -P '.' s.json |
plik s.json |
JSON (z wcięciami) | pułapka przy przekierowaniu do .yaml |
yq -o=yaml '.' s.json |
plik s.json |
YAML (styl blokowy) | poprawne |
yq -P -o=yaml '.' s.json |
plik s.json |
YAML (styl blokowy) | poprawne, tu -P jest zbędne |
cat s.json | yq -P '.' |
stdin, bez rozszerzenia | YAML (styl blokowy) | poprawne, bo domyślnie yaml |
Trzeci wiersz mówi jedną rzecz: gdy -o=yaml już jest, -P nie zawsze jest potrzebne. Ale nie jest też nieszkodliwe — patrz dane o czasie w sekcji szóstej. Czwarty wiersz wyjaśnia, dlaczego „przepisałem i działa”: wiele osób podaje dane przez potok, co przypadkiem omija gałąź z rozszerzeniem.
3. Konwersja wsadowa: trzy warianty dla shella
Konwersja jednego pliku to dopiero punkt wyjścia. Prawie każdy realny powód, by robić to w wierszu poleceń, dotyczy partii: wszystkie konfiguracje w katalogu, wszystkie zrzucone snapshoty API, wszystko naraz do YAML.
Pętla bash / zsh (najbardziej czytelna, dla jednego katalogu):
for f in configs/*.json; do
yq -o=yaml '.' "$f" > "${f%.json}.yaml"
done
find, rekurencyjnie (gdy liczą się podkatalogi):
find . -name '*.json' -exec sh -c 'yq -o=yaml "." "$1" > "${1%.json}.yaml"' _ {} \;
PowerShell (na Windows CI):
Get-ChildItem *.json | ForEach-Object { yq -o=yaml '.' $_.FullName > ($_.BaseName + '.yaml') }
Wszystkie trzy warianty mają jeden twardy warunek wstępny: najpierw upewnij się, że źródłowy JSON jest poprawny. Konwerter to nie parser. W przypadku błędu składni wiele konwerterów podaje tylko mgliste położenie zamiast szczegółów z numerami wierszy, które daje walidator. Dodanie jednej kontroli wewnątrz pętli kosztuje prawie nic:
for f in configs/*.json; do
jq empty "$f" || { echo "invalid: $f"; continue; }
yq -o=yaml '.' "$f" > "${f%.json}.yaml"
done
jq empty tylko parsuje i nic nie wypisuje, a dla niepoprawnego JSON zwraca niezerowy kod wyjścia. W połączeniu z set -euo pipefail zatrzymuje to cały potok na pierwszym uszkodzonym pliku, zamiast wyprodukować stertę niedokończonych wyników.
4. Cztery opcje wyjścia, cztery różne zadania
Lista opcji yq jest długa, ale na trasie JSON → YAML trzeba zapamiętać tylko cztery:
| Opcja | Co robi | Kiedy trzeba ją podać |
|---|---|---|
-o=yaml / -oy |
Ustawia format wyjścia na YAML | Zawsze, gdy wejściem jest plik .json |
-P / --prettyPrint |
Wymusza styl blokowy i normalizuje wcięcia | Odczyt ze stdin lub gdy liczą się wcięcia sekwencji |
-I=n |
Szerokość wcięcia, domyślnie 2 | Downstream wymaga 4 spacji (niektóre konwencje Ansible) |
--split-exp |
Rozbija elementy tablicy na dokumenty | Generowanie wielozasobowych manifestów Kubernetes |
Wątek wielu dokumentów zasługuje na osobne zdanie. Manifesty Kubernetes zwyczajowo oddzielają zasoby przez ---. Generując to z tablicy JSON:
yq -P -o=yaml '.[] | splitDoc' --split-exp 'true' kubernetes-list.json
Bez --split-exp dostaniesz jedną sekwencję YAML; z nim każdy element tablicy staje się osobnym dokumentem rozdzielonym ---. kubectl apply przyjmie oba warianty, ale czytelność diffów różni się o rząd wielkości.
5. Cudzysłowy: sześć rodzajów łańcuchów, które trzeba cytować
Wnioskowanie typów w YAML jest niejawne. Wartość łańcuchowa, która wygląda jak liczba lub wartość logiczna, bez cudzysłowów zostanie sparsowana jako inny typ. To nie jest ryzyko teoretyczne: zmierzono na yq v4.44.6, że wszystkie sześć poniższych wartości dostaje cudzysłowy automatycznie, co dowodzi, że parser naprawdę by je źle odczytał:
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"
Skutki, po kolei:
| Wartość oryginalna (łańcuch JSON) | Jak zostanie odczytana bez cudzysłowów | Realny wpływ |
|---|---|---|
"1.0" |
Liczba zmiennoprzecinkowa 1.0 |
Wersja staje się ułamkiem; 1.10 zwija się do 1.1 |
"true" |
Wartość logiczna true |
Logika rozgałęzień odwraca się albo typy się nie zgadzają |
"8080" |
Liczba całkowita 8080 |
Typ portu się zmienia, porównanie łańcuchów kończy się niepowodzeniem |
"" |
null |
Pusty łańcuch znika, downstream dostaje None |
"host: value" |
Błąd parsowania lub zagnieżdżone mapowanie | Po dwukropku musi być spacja, inaczej błąd składni |
"2026-11-03" |
Obiekt daty | Deserializuje się do datetime.date, nie do łańcucha |
Ostatni punkt jest najbardziej zdradliwy. W JSON data to po prostu łańcuch; po konwersji staje się typem daty YAML, a przy powrocie do JSON wiele implementacji wypisuje obiekt daty w formacie ISO zamiast oryginalnego łańcucha — spójność w obie strony zostaje przerwana. Zarówno PyYAML, jak i yq dodają tu cudzysłowy, co jest równoznaczne z przyznaniem, że ryzyko jest realne.
Przy okazji: wiersz unicode: 端口 8080 jest wypisany bez cudzysłowów — tekst chiński jest w YAML bezpieczny. To nie on powinien cię martwić, lecz kolejność kluczy i precyzja, o których mowa dalej.
6. Odpowiedniki w Pythonie i Node wraz ze zmierzonym czasem
Nie wszędzie da się zainstalować yq: środowiska odcięte od sieci, zamknięte obrazy CI, Windows bez menedżera pakietów. Odpowiedniki w Pythonie i Node istnieją i każdy ma własne wartości domyślne gotowe ugryźć. Pełna tabela porównawcza trzech ścieżek pod kątem scenariuszy znajduje się w artykule o wyborze narzędzi CLI i edytorów JSON; ten tekst skupia się wyłącznie na kierunku JSON → YAML.
Python + PyYAML (trzy argumenty, żadnego nie można pominąć):
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, # styl blokowy, nie {a: 1, b: 2}
allow_unicode=True, # nie escapuj chińskiego do \u7AEF\u53E3
sort_keys=False, # zachowaj oryginalną kolejność kluczy JSON
)
Te trzy mają wartości domyślne False / False / True, co oznacza, że PyYAML prosto z pudełka układa klucze alfabetycznie i zamienia chiński tekst na \uXXXX. Zmierzone wyjście dla tego samego wejścia przy wartościach domyślnych:
apiVersion: v1
data:
LOG_LEVEL: info
colon: 'host: value'
unicode: "\u7AEF\u53E3 8080"
kind: ConfigMap
metadata:
labels:
app: web
name: app-config
Zwróć uwagę, że kind wylądowało teraz po apiVersion, a przed metadata. W manifestach Kubernetes konwencją społeczności jest, że apiVersion stoi przed kind. Przestawienie kluczy nie psuje parsowania, ale sprawia, że recenzja jest udręką.
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 wyłącza zawijanie długich linii (inaczej długie URL-e są rozrywane w połowie), a noRefs: true zakazuje generowania kotwic i aliasów — wymagane, gdy konsumentem jest kubectl lub inne narzędzie, które ich nie rozwiązuje.
Zmierzone czasy (próbka: 2.39 MB JSON, 12000 rekordów, każdy z zagnieżdżonym obiektem i dwiema tablicami; maszyna to sandbox użyty do tego zadania):
| Implementacja | Czas konwersji | Czas parsowania wstecz | Spójność w obie strony |
|---|---|---|---|
yq -o=yaml v4.44.6 |
0.45 s | 0.45 s | identyczna |
yq -P -o=yaml v4.44.6 |
4.16 s | — | identyczna |
| Node js-yaml 4.x | 0.23 s | 0.11 s | identyczna |
| Python PyYAML 6.0.3 | 3.26 s | 8.01 s | identyczna |
Dwie kontrintuicyjne rzeczy. Po pierwsze, z -P yq zwalnia około 9 razy (0.45 s → 4.16 s), mimo że na tej próbce wyjście jest identyczne co do bajtu (2,326,895 bajtów w obu przypadkach) — -P wykonuje pełny przebieg ponownego formatowania i to się sumuje; przy konwersji setek plików lepiej tego unikać. Po drugie, parsowanie wsteczne w PyYAML (8.01 s) jest ponad dwukrotnie wolniejsze niż konwersja w przód (3.26 s), więc Python jest kiepskim wyborem, jeśli potok wczytuje YAML raz za razem.
I jeszcze jedno: 2.39 MB JSON po zamianie na blokowy YAML waży 2.33 MB, czyli jest około 3% mniejszy. Rozpowszechniony mit, że „YAML zawsze jest większy niż JSON”, nie wytrzymuje próby — przy głębokim zagnieżdżeniu cudzysłowy, przecinki i nawiasy klamrowe JSON mogą ważyć więcej niż wcięcia YAML.
7. Trzy rzeczy, które nie wracają
Poprzednie sekcje mówiły, jak konwertować poprawnie. Ta mówi o tym, co ginie. Wejście skonstruowane specjalnie, wyniki zmierzone:
printf '{"order_id":12345678901234567890,"zip":"0150","ts":"2026-11-03","f":1.0e6}' > t.json
yq -o=yaml '.' t.json
Wyjście yq:
order_id: 1.2345678901234567e+19
zip: "0150"
ts: "2026-11-03"
f: 1000000
A po powrocie do JSON: {"order_id":12345678901234567000,"zip":"0150","ts":"2026-11-03","f":1000000}.
20-cyfrowy numer zamówienia 12345678901234567890 zmienił się w 12345678901234567000 — trzy ostatnie cyfry wyzerowane, bez żadnego ostrzeżenia. Powód: yq idzie ścieżką parsowania float64 z Go, liczby całkowite powyżej 2^53 nie dają się przedstawić dokładnie, więc włącza się wyjście w notacji naukowej i w momencie powrotu oryginału już nie ma.
To samo wejście przez PyYAML:
order_id: 12345678901234567890
zip: '0150'
ts: '2026-11-03'
f: 1000000.0
Liczby całkowite w Pythonie mają dowolną precyzję, więc wszystkie 20 cyfr zostaje, a ponowny odczyt daje 12345678901234567890. Jeśli w twoich danych są numery zamówień, snowflake id czy numery dokumentów tożsamości, nie używaj yq — użyj Pythona albo najpierw zamień to pole na łańcuch. Pełny mechanizm tej klasy problemów z precyzją wyjaśnia artykuł dlaczego duże liczby całkowite w JSON tracą precyzję.
Drugą stratą jest rozróżnienie float i int: 1.0e6 w wyjściu yq staje się 1000000 (całkowitą), a PyYAML zachowuje 1000000.0. Trzecią jest kolejność kluczy — PyYAML domyślnie sortuje, yq i js-yaml domyślnie zachowują, co w zespołach mieszanych językowo jest ukrytym źródłem niezgodności.
8. W CI nie tylko konwertuj: walidacja → konwersja → lint
Traktowanie konwersji jako jedynego kroku to najczęstsza wada potoku. Zerowy kod wyjścia oznacza tylko „wejściowy JSON dał się sparsować”, a nie „wyjście jest poprawne” ani „wyjście spełnia wymagania konsumenta”. Prawidłowa, trójetapowa postać:
#!/usr/bin/env bash
set -euo pipefail
# 1. Walidacja źródła: warstwa składni
jq empty deployment.json
# 2. Konwersja: jawnie podaj format wyjścia
yq -o=yaml '.' deployment.json > deployment.yaml
# 3. Walidacja artefaktu: czytelny dla parsera YAML i zgodny z regułami
yq -o=json -I=0 '.' deployment.yaml > /dev/null
yamllint deployment.yaml
Ten yq -o=json ... > /dev/null w kroku 3 wygląda na zbędny, a jest najużyteczniejszą linią całego bloku: wymusza ponowne sparsowanie artefaktu. Błędy wcięć, nieudane rozwiązanie kotwic i przepisane typy wychodzą właśnie tutaj. Pomiń to, a problem odłoży się do kubectl apply albo do startu aplikacji.
Jeśli artefakt to konfiguracja utrzymywana przez ludzi (Helm values, docker-compose, workflow GitHub Actions), sprawdź dodatkowo trzy rzeczy: czy kolejność kluczy nie została przestawiona, czy długie liczby nie straciły cyfr i czy komentarze trzeba dopisać ręcznie — JSON nie ma komentarzy, więc każdą adnotację trzeba dodać po konwersji; żaden konwerter nie wymyśli ich za ciebie.
FAQ
yq -P czy -o=yaml — co właściwie pisać?
-o=yaml decyduje, czy wyjście jest YAML-em; -P decyduje, jak ten YAML wygląda. Gdy wejściem jest plik .json, -o=yaml jest obowiązkowe, a -P dodajesz tylko wtedy, gdy potrzebujesz wymuszonego stylu blokowego lub wcięć sekwencji. Przy odczycie ze stdin bez rozszerzenia yq i tak domyślnie wypisuje YAML, więc samo -P wystarcza.
Czy przekonwertowany YAML można podać prosto do Kubernetes?
Składniowo tak, ale pod trzema warunkami: klucze mające konwencję kolejności, takie jak apiVersion, nie zostały przestawione (PyYAML sortuje domyślnie, ustaw sort_keys=False); nie powstały kotwice ani aliasy (js-yaml: ustaw noRefs: true); długie liczby zachowały cyfry. Przy wielozasobowych manifestach rozbij na dokumenty oddzielone --- przy użyciu --split-exp.
Dlaczego moje wyjście to jedna linia {a: 1, b: 2}, a nie z wcięciami?
To styl flow. Jest poprawnym YAML-em, ale niweczy sens konwersji. Użyj -P, by wymusić styl blokowy; jeśli jesteś na PyYAML, upewnij się, że default_flow_style=False (od PyYAML 5.1 wartość domyślna to już False, starsze wersje wymagają jawnego zapisu).
Mój 20-cyfrowy numer zamówienia stracił ostatnie cyfry. Da się je odzyskać?
Nie — można tylko zapobiegać. yq parsuje przez float64, więc liczby całkowite powyżej 2^53 tracą precyzję z konstrukcji. Albo przejdź na Pythona (liczby całkowite o dowolnej precyzji; sprawdzono, bezstratne), albo zapisuj długie liczby jako łańcuchy już w JSON — to drugie jest pewniejsze, bo rozwiązuje też ten sam problem JSON.parse po stronie JavaScript.
Konwerter online czy wiersz poleceń?
Konfiguracje z danymi wrażliwymi i snapshoty API z tokenami konwertuj lokalnym wierszem poleceń lub narzędziem działającym po stronie klienta — nigdy nie wysyłaj ich na serwer. Do jednorazowych, małych danych, które chcesz potwierdzić wzrokowo, narzędzie online jest szybsze; strona /convert/yaml na tej witrynie działa w całości w twojej przeglądarce, a dane nie opuszczają twojej maszyny. Do wszystkiego, co jest wsadowe, ma być odtwarzalne lub wchodzi do CI, używaj wiersza poleceń i wpisz kroki walidacji do tego samego skryptu.