JSON Toolkit

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, объясняется, почему формат вывода определяет расширение входа, и даются верные рецепты пакетного преобразования, кавычек, отступов и точности больших целых чисел.

Средний~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 никогда не сводилось к простой смене расширения файла. Настоящая цена прячется в четырёх местах: формат вывода определяет расширение входа, правила кавычек молча переписывают ваши строки, отступы последовательностей влияют на инструменты, которые читают результат дальше, а большое целое число, один раз прошедшее через 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 используйте командную строку и запишите шаги проверки в тот же самый скрипт.