JSON Toolkit

Конвертація JSON у YAML у командному рядку: чотири пастки за однією командою yq

Головне: Найчастіше копійована в підручниках команда yq -P input.json > output.yaml насправді все ще видає JSON. У статті yq v4.44.6, PyYAML 6.0.3 і js-yaml прогнані на зразку 2.39 MB, пояснюється, чому формат виводу визначає розширення вхідного файла, і даються правильні рецепти для пакетної конвертації, лапок, відступів і точності великих цілих чисел.

Середній~9 хв читанняОпубліковано 2026-11-03Оновлено 2026-11-03
Обкладинка з порівнянням команди конвертації JSON у YAML та її виводу в терміналі

Висновок: yq -P input.json > output.yaml — команда, яку найчастіше копіюють з підручників, — на yq v4 усе ще продукує JSON. Щоб отримати YAML, треба явно передати -o=yaml (або -oy). -P — це скорочення від --prettyPrint: воно керує відступами й блоковим стилем, а не форматом виводу.

Конвертація JSON у YAML ніколи не зводилася до «змінити розширення файла». Справжня ціна ховається в чотирьох місцях: формат виводу визначає розширення вхідного файла, правила лапок непомітно переписують ваші рядки, відступи послідовностей впливають на downstream-інструменти, а велике ціле число, пройшовши через 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

Третій рядок каже одне: коли вже є -o=yaml, -P не завжди потрібен. Але нешкідливим його теж не назвеш — дивіться дані про час у шостому розділі. Четвертий рядок пояснює, чому «я списав і воно працює»: багато хто подає дані через pipe, і це випадково оминає гілку з розширенням.

3. Пакетна конвертація: три варіанти для shell

Конвертація одного файла — це лише старт. Майже кожна реальна причина робити це в командному рядку — пакетна: усі конфігурації в каталозі, усі збережені знімки API, і все одразу в YAML.

Цикл 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 (на Windows CI):

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 це зупиняє весь конвеєр на першому зіпсованому файлі, а не видає купу напівконвертованих результатів.

4. Чотири параметри виводу — чотири різні задачі

Список параметрів yq довгий, але на шляху JSON → YAML запам'ятати треба лише чотири:

Параметр Що робить Коли обов'язково передавати
-o=yaml / -oy Задає формат виводу YAML Завжди, коли вхід — файл .json
-P / --prettyPrint Примусово блоковий стиль, нормалізує відступи Читання з stdin або коли важливі відступи послідовностей
-I=n Ширина відступу, типово 2 Downstream вимагає 4 пробіли (деякі Ansible style guide)
--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" Число з плаваючою комою 1.0 Версія стає дробовою; 1.10 перетворюється на 1.1
"true" Булеве true Логіка умов інвертується або типи не збігаються
"8080" Ціле 8080 Тип порту змінюється, порівняння рядків провалюється
"" null Порожній рядок зникає, downstream отримує None
"host: value" Помилка парсингу або вкладене відображення Після двокрапки обов'язковий пробіл, інакше синтаксична помилка
"2026-11-03" Об'єкт дати Десеріалізується в datetime.date, а не в рядок

Останній пункт найпідступніший. У JSON дата — це просто рядок; після конвертації вона стає типом дати YAML, і під час зворотної конвертації багато реалізацій видають об'єкт дати у форматі ISO замість початкового рядка — узгодженість зворотного перетворення розривається. І PyYAML, і yq додають тут лапки, що рівнозначно визнанню цього ризику реальним.

До речі, рядок unicode: 端口 8080 виводиться без лапок — китайський текст у YAML безпечний. Хвилюватися треба не за нього, а за порядок ключів і точність, про які йдеться далі.

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,        # не екранувати китайське в \u7AEF\u53E3
        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 або інший інструмент, що не розкриває їх.

Виміряний час (зразок: 2.39 MB JSON, 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

А після зворотної конвертації: {"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 у виводі yq стає 1000000 (цілим), а PyYAML зберігає 1000000.0. Третя — порядок ключів: PyYAML типово переставляє, yq і js-yaml типово зберігають, і в міжмовній співпраці це приховане джерело розбіжностей.

8. У CI не просто конвертуйте: валідація → конвертація → lint

Вважати конвертацію єдиним кроком — найпоширеніший дефект конвеєра. Нульовий код виходу означає лише «вхідний JSON розпарсився», а не «вивід правильний» і не «вивід задовольняє споживача». Правильна триступенева форма:

#!/usr/bin/env bash
set -euo pipefail

# 1. Валідація джерела: синтаксичний шар
jq empty deployment.json

# 2. Конвертація: явно задаємо формат виводу
yq -o=yaml '.' deployment.json > deployment.yaml

# 3. Валідація артефакту: читається парсером YAML і відповідає правилам
yq -o=json -I=0 '.' deployment.yaml > /dev/null
yamllint deployment.yaml

Той yq -o=json ... > /dev/null у кроці 3 виглядає зайвим, але це найкорисніший рядок усього блоку: він примусово перепарсює артефакт. Помилки відступів, збої розкриття якорів і переписані типи виявляються саме тут. Пропустіть його — і проблема відкладеться до kubectl apply або запуску застосунку.

Якщо артефакт — це конфігурація, яку підтримують люди (Helm values, 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.

Чому мій вивід — один рядок {a: 1, b: 2}, а не з відступами?

Це flow style. Він валідний, але знецінює саму конвертацію. Використайте -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 використовуйте командний рядок і вписуйте кроки валідації в той самий скрипт.