JSON в YAML из командной строки: четыре ловушки одной команды yq
Главное: Самая копируемая команда из учебников, yq -P input.json > output.yaml, на yq v4 всё равно выдаёт JSON. В статье на выборке 2.39 MB замерены yq v4.44.6, PyYAML 6.0.3 и js-yaml, объясняется, почему формат вывода определяет расширение входа, и даются верные рецепты пакетного преобразования, кавычек, отступов и точности больших целых чисел.

Коротко: 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), поэтому сразу ничего не сломается. Оно сломается в тот день, когда кто-то добавит комментарий или якорь, либо когда yamllint в CI начнёт выдавать целую стену сообщений truthy value should be one of. Особенно неприятно это в общих репозиториях: такой файл месяцами проходит ревью, потому что визуально выглядит как обычный YAML, а ошибка всплывает только при первом же изменении структуры, когда доступа к исходному JSON уже нет.
Команда, которая действительно выдаёт 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 |
Третья строка показывает: -P не всегда нужен, когда уже есть -o=yaml, но он и не безобиден — см. замеры в шестом разделе. Четвёртая объясняет, почему «я просто скопировал команду, и всё работает»: многие передают данные через конвейер и случайно обходят ветку, где участвует расширение. Если перенести такую команду из интерактивного терминала в скрипт с явным именем файла, поведение меняется на противоположное. Именно поэтому стоит всегда писать -o=yaml явно, даже когда сегодня данные идут по конвейеру: это делает результат независимым от того, как именно в следующий раз подключат входные данные.
3. Пакетное преобразование: три варианта для shell
Преобразование одного файла — только начало. Почти любая реальная причина делать это в командной строке связана с пакетной обработкой: все конфигурации в каталоге, все снимки интерфейсов, всё за один проход.
Цикл 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 весь конвейер остановится на первом же плохом файле, а не произведёт груду наполовину преобразованных результатов. Для ночных сборок, которые обрабатывают сотни конфигураций, это разница между одной понятной ошибкой в журнале и десятками битых YAML, которые потом придётся разгребать вручную. Полезно также сразу решить, что делать с уже существующими .yaml файлами: цикл перезапишет их без предупреждения, поэтому в репозиториях со смешанными форматами стоит добавить проверку перед перенаправлением вывода.
4. Четыре параметра вывода, четыре разные задачи
Список параметров yq длинный, но на пути JSON → YAML действительно важны только четыре:
| Параметр | Что делает | Когда его обязательно писать |
|---|---|---|
-o=yaml / -oy |
задаёт формат вывода YAML | когда на входе файл .json |
-P / --prettyPrint |
принудительно включает блочный стиль и нормализует отступы | при чтении из stdin или когда важны отступы последовательностей |
-I=n |
число пробелов отступа, по умолчанию 2 | downstream требует 4 пробела (часть стилевых руководств Ansible) |
--split-exp |
разбивает элементы массива на отдельные документы | при генерации манифестов Kubernetes с несколькими ресурсами |
Про --split-exp стоит сказать отдельно. Манифесты Kubernetes принято разделять отдельные ресурсы маркером ---. При генерации такого файла из массива JSON:
yq -P -o=yaml '.[] | splitDoc' --split-exp 'true' kubernetes-list.json
Без --split-exp получится одна YAML-последовательность; с ним каждый элемент массива станет отдельным документом, разделённым ---. Оба файла kubectl apply примет без возражений, но читаемость diff отличается на порядок. В командах, где манифесты просматривают люди перед выкаткой, именно эта разница решает, заметят ли лишнее поле в resources до того, как оно попадёт в кластер.
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 подставляют таким значениям кавычки именно поэтому: они фактически признают, что риск реален. Чаще всего это всплывает в конфигурациях с полями вроде release_date или valid_until, которые меняют тип молча, без единого предупреждения в журнале сборки, и ловится уже на прикладном коде, который ожидал строку.
Кстати, строка 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, # 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 сортирует ключи по алфавиту и превращает китайский текст в \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; перестановка не ломает разбор, но делает ревью мучительным. В командах, где манифесты сравнивают глазами на каждом pull request, это быстро превращается в источник бессмысленных 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 запрещает генерацию якорей и псевдонимов — это обязательно, когда потребителем выступает kubectl или другой инструмент, который не умеет разрешать якоря. Обе опции относятся к тем же умолчаниям, что и sortKeys: false: все три приходится задавать явно, потому что библиотека оптимизирована под компактный вывод, а не под конфигурацию, которую будет читать человек.
Замеры времени (образец: JSON объёмом 2.39 MB, 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 после преобразования в блочный YAML занимают 2.33 MB, то есть примерно на 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
А при обратном преобразовании в 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 храните такое поле строкой. Полный механизм этого класса ошибок разобран в почему большие целые числа в JSON теряют точность.
Вторая потеря — различие между float и int: 1.0e6 в выводе yq становится 1000000 (целым), тогда как PyYAML сохраняет 1000000.0. Третья потеря — порядок ключей: PyYAML по умолчанию сортирует, а yq и js-yaml по умолчанию сохраняют, и в команде, где используются разные языки, это скрытый источник расхождений. Все три объединяет одно: команда завершается с кодом 0, поэтому заметить потери можно только прямым сравнением входа и выхода, а не по статусу выполнения.
8. В CI мало просто convert: проверка → преобразование → 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 выглядит лишней, а на деле это самая полезная строка во всём блоке: она принудительно заставляет заново разобрать готовый файл. Ошибки отступов, неудачно разрешённые якоря и переписанные типы всплывают именно здесь. Пропустите её — и проблема дождётся kubectl apply или запуска приложения, где цена ошибки уже совсем другая.
Если результатом становится конфигурация, которую поддерживают люди (Helm values, docker-compose, рабочие процессы GitHub Actions), проверьте дополнительно три вещи: не переставлен ли порядок ключей, не потеряли ли длинные числа разряды и не нужно ли добавить комментарии вручную. В JSON комментариев нет, поэтому любые пояснения придётся писать уже после преобразования; ни один преобразователь не создаст их за вас. Хорошая практика — хранить рядом с генерируемым файлом короткую памятку о том, какие поля требуют ручной проверки, чтобы следующий участник команды не принял автогенерированный YAML за окончательный источник истины.
Вопросы и ответы
Что писать: 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}, а не с отступами?
Это потоковый стиль. Он допустим, но сводит смысл преобразования к нулю. Используйте -P, чтобы принудительно включить блочный стиль; если работаете с PyYAML, проверьте default_flow_style=False (начиная с PyYAML 5.1 это значение по умолчанию, в более старых версиях его нужно указывать явно).
У 20-значного номера заказа обнулились последние цифры. Можно ли их вернуть?
Нет, можно только предотвратить. yq разбирает числа через float64, поэтому целые больше 2^53 теряют точность по определению. Либо переходите на Python (целые произвольной точности, проверено без потерь), либо храните длинные числа строками прямо в JSON — последнее надёжнее, потому что заодно решает такую же проблему с JSON.parse на стороне JavaScript.
Онлайн-конвертер или командная строка?
Для конфигураций с секретами и снимков API с токенами используйте локальную командную строку или чисто клиентский инструмент и ничего не загружайте на сервер. Для разовых небольших данных, которые хочется визуально проверить, онлайн-инструмент быстрее; страница /convert/yaml на этом сайте работает целиком в браузере, и данные не покидают вашу машину. Для всего пакетного, воспроизводимого и встроенного в CI используйте командную строку и запишите шаги проверки в тот же самый скрипт.