Skip to content
Назад к блогу
Руководства

Норвежская проблема YAML и различия JSON-YAML для инженеров

Почему YAML читает «no» как false. Реальные сбои K8s от кавычек. JSON против YAML, правила отступов и конвертация манифестов K8s.

14 мин чтения

Норвежская проблема YAML и различия JSON ↔ YAML, которые должен знать инженер

Это был обычный Helm-деплой. Команда два дня настраивала values.yaml для мультирегионального раскатывания. Чарт шаблонизировал ConfigMap Kubernetes с локальными метаданными — включая код страны для норвежского data-центра. Кто-то набрал country: NO и закоммитил. CI прошёл зелёным. Деплой ушёл.

А потом пришли алерты.

В ConfigMap оказалось country: false вместо country: "NO". Каждый downstream-сервис, читавший поле страны, получил boolean вместо строки. Сравнение строк сломалось. Логика маршрутизации провалилась в дефолт. Трафик, который должен был остаться в Норвегии, ушёл на не тот региональный endpoint.

Корневая причина — одна строка без кавычек в YAML-файле. YAML 1.1 — версия, которую использует практически весь Kubernetes-инструментарий — трактует NO как boolean false. И ещё трактует так же YES, ON, OFF, Y, N, no, yes, on, off, y, n и десяток других вариантов. Без предупреждения. Без ошибки. Тихо и неправильно.

В JSON такой проблемы нет. {"country": "NO"} — это всегда строка. Неявное приведение типов в YAML — одновременно его главное удобство и самая опасная мина.

В этом руководстве — полная картина: почему норвежская проблема существует, что изменилось в YAML 1.2 (и почему почти все её игнорируют), как правильно расставлять кавычки, какие правила отступов сбивают новичков, ловушки точности чисел и четыре реальных сценария конвертации — от манифестов Kubernetes до планов Terraform. Когда нужно безопасно перевести JSON-значение в YAML без этой ловушки, наш конвертер JSON в YAML автоматически берёт в кавычки строки, склонные к норвежской проблеме.

JSON против YAML — когда какой формат

Перед тем как погружаться в норвежскую проблему, полезно понять, на что каждый формат реально оптимизирован. Они не взаимозаменяемы — у каждого свой центр тяжести, который делает его лучшим выбором в конкретных контекстах.

ПараметрJSONYAML
СинтаксисСтрогий — фигурные скобки, кавычки, запятые обязательныГибкий — отступы и минимум пунктуации
Система типовЯвная: string, number, boolean, null, array, objectНеявная — YAML 1.1 выводит тип из формы значения
ЧитаемостьДружелюбен разработчику, машинно проверяемыйДружелюбен человеку, удобно править вручную
КавычкиСтроки всегда в кавычкахБольшинство скаляров можно без кавычек (источник «норвежца»)
КомментарииНе поддерживаютсяПоддерживаются через #
ПрименениеAPI, обмен данными, современные системы конфиговKubernetes, Docker Compose, Ansible, CI-конвейеры
Сюрпризы парсингаНет — строгий парсингЕсть — Norway, восьмеричные, timestamp
Принуждение схемыЭкосистема JSON SchemaYAML Schema (меньше инструментов)

JSON выигрывает, когда данные пересекают границы систем — REST API, очереди сообщений, сериализация для БД. Машины их разбирают, машины их генерируют, а строгий синтаксис делает валидацию простой. Применяйте форматировщик JSON, чтобы проверить структуру до отправки.

YAML выигрывает, когда основные авторы — люди. Манифесты Kubernetes, GitHub Actions workflows, Helm-чарты, Ansible-плейбуки — это файлы, которые разработчики читают и правят десятки раз. Сниженная пунктуация и поддержка комментариев делают их реально удобнее в сопровождении, чем JSON-эквиваленты.

Проблема возникает на границе: когда инструмент генерирует JSON (например, kubectl get deploy -o json или terraform show -json), а человеку нужно положить результат в git и редактировать как YAML. Именно в этой конвертации живёт норвежская проблема. Наш конвертер YAML в JSON закрывает обратное направление, когда нужно вернуться обратно.

Норвежская проблема — глубокий разбор

Норвежская проблема — не баг. Это фича спецификации YAML 1.1, ведущей себя ровно так, как заложено. Понимание того, почему так задумано — и почему так много систем до сих пор реализуют 1.1 — ключ к тому, чтобы её обходить.

Почему «no», «yes», «on», «off», «y», «n» разбираются неверно

Спецификация YAML 1.1 определяла широкий boolean-тип, задуманный быть человеко-дружественным. Она распознавала всё нижеперечисленное как true или false:

True: y, Y, yes, Yes, YES, true, True, TRUE, on, On, ON

False: n, N, no, No, NO, false, False, FALSE, off, Off, OFF

Намерение было хорошим: конфиги часто пишут yes/no вместо true/false по-английски, и YAML хотел поддержать естественный для людей способ. Проблема в том, что yes, no, on, off, y, n — это и совершенно легитимные строковые значения, означающие в большинстве приложений нечто совсем другое.

Вот рассогласование на конкретном YAML:

# YAML 1.1 (что реализует большинство парсеров)
country: NO        # парсится как: country: false   ← ОПАСНО
enabled: yes       # парсится как: enabled: true
restart: off       # парсится как: restart: false
language: y        # парсится как: language: true
shell: n           # парсится как: shell: false

# Правильно — явные строковые кавычки переопределяют вывод типа
country: "NO"      # парсится как: country: "NO"    ← безопасно
enabled: "yes"     # парсится как: enabled: "yes"
restart: "off"     # парсится как: restart: "off"
language: "y"      # парсится как: language: "y"
shell: "n"         # парсится как: shell: "n"

И сравнение с JSON:

{"country": "NO"}

В JSON NO в кавычках всегда и безусловно строка. Никакого неявного вывода типа. Та же строгость, делающая JSON многословным, делает его и безопасным.

Кроме boolean-приведения, YAML 1.1 неявно конвертирует:

  • 123e4 → число 1230000 (научная нотация)
  • 0x1A → число 26 (шестнадцатеричное)
  • 0755 → число 493 (восьмеричное — это ломает Unix-права на файлы)
  • 2024-05-04 → объект даты во многих парсерах (а не строка)
  • 1_000_000 → число 1000000 (подчёркивание-разделитель)

Норвежская проблема — лишь самый известный представитель целого семейства неявных приведений в YAML.

YAML 1.1 против 1.2 — что изменилось

YAML 1.2 опубликован в 2009 — спустя четыре года после 1.1. Главная цель — привести YAML в строгое соответствие с JSON (поскольку JSON фактически валидное подмножество YAML 1.2) и сократить удивительные неявные конвертации типов.

В YAML 1.2:

  • Boolean сужен ровно до true и false (с учётом регистра). И всё. yes, no, on, off — обычные строки.
  • Восьмеричные литералы требуют префикс 0o (0o755) — старая форма 0755 — строка.
  • Timestamp не разбираются неявно — 2024-05-04 остаётся строкой, пока вы не пометите её явно.
  • Сама спецификация — надмножество JSON: всякий валидный JSON-документ — валидный YAML 1.2.

На бумаге YAML 1.2 решает норвежскую проблему полностью. На практике экосистема почти не сдвинулась.

БиблиотекаДефолтная спецификацияРиск Norway
PyYAML (Python)YAML 1.1Да — yaml.safe_load всё ещё парсит NO как False
ruamel.yaml (Python)YAML 1.2 (опционально)Настраиваемо — безопаснее по умолчанию
js-yaml (Node.js)YAML 1.1Да в старых версиях; в новых есть опция FAILSAFE_SCHEMA
eemeli/yaml (Node.js)YAML 1.2Нет — 1.2 по умолчанию или с явным выбором версии
gopkg.in/yaml.v2 (Go)YAML 1.1Да
gopkg.in/yaml.v3 (Go)YAML 1.2Существенно безопаснее
Kubernetes / HelmYAML 1.1 (через Go yaml.v2)Да — историческая, очень тяжёлая миграция
AnsibleYAML 1.1 (через PyYAML)Да

Причина медленной миграции — обратная совместимость. Системы, десятилетиями полагавшиеся на парсинг yes/no как boolean, не могут тихо сменить поведение, не сломав существующие конфиги. Kubernetes в особенности — массивный установленный парк, в котором смена YAML-семантики стала бы breaking change по всему кластеру.

Практический вывод: предполагайте семантику YAML 1.1 в любом инструменте, который вы явно не настроили иначе. Всегда берите в кавычки строки, которые могут быть прочитаны как boolean, timestamp или числа.

Как production-системы попадаются

Норвежский код страны — самый известный пример, потому что он контринтуитивный — NO выглядит очевидной аббревиатурой, а не boolean. Но паттерн повторяется во многих реальных сценариях:

Коды аэропортов IATA. Норвежский аэропорт Harstad/Narvik имеет код EVE. Безопасно. Oslo Gardermoen — OSL. Тоже безопасно. Но любое приложение, хранящее в YAML региональные коды аэропортов, в одном no маршрутном коде от того, чтобы получить boolean false в продакшене.

Имена переменных окружения. ON — вполне валидное значение переменной окружения, означающее «включено» в некоторых легаси-системах. OFF — его пара. Миграция конфигов из shell-скриптов в YAML без кавычек этих значений вносит тихое приведение типа.

Поля email-пользователей. Пользователь, чьё имя или username буквально n, y или любое из триггерных слов, сериализуется неверно, если приложение пишет YAML без должных кавычек. Это особенно коварно — отказ только для подмножества пользователей.

Политики перезапуска Docker Compose. Значение "no" в restart_policy — валидное и означает «не перезапускать контейнер». Если оно потеряет кавычки в YAML round-trip, значение станет false, и Docker Compose может либо интерпретировать его как «политика не задана», либо бросить ошибку валидации — в обоих случаях поведение перезапуска неверное.

Поле shell: в GitHub Actions. Валидные значения — bash, pwsh, python, sh, cmd, powershell. Никакое из них не норвежское слово. Но кто-то, набравший shell: yes или shell: on как заглушку при правках, удивится, когда YAML превратит это в boolean ещё до того, как валидатор вообще увидит файл.

Решение во всех случаях одно: брать в кавычки строки, которые семантически — строки, независимо от того, узнал бы человек в них ключевое слово. Наш конвертер JSON в YAML применяет это автоматически — любое значение из списка норвежских слов в выводе оказывается в кавычках.

Стратегия кавычек для строк

Когда вы поняли, почему норвежские слова рассогласуются, остаётся выбрать правильную стратегию кавычек под задачу. YAML поддерживает три режима с разными trade-off.

Auto, double и single

Авто-кавычки (рекомендуется для большинства конвертаций) — пусть библиотека сама решает, когда кавычки нужны. Значения, которые без кавычек прочитались бы неверно — норвежские слова, числа, timestamp, строки, похожие на YAML-синтаксис, — берутся в кавычки автоматически. Всё остальное остаётся обычным скаляром. Это даёт самый читаемый вывод и при этом безопасно.

# Вывод в режиме auto
name: Alice          # обычный — нет неоднозначности
country: "NO"        # в кавычках — норвежское слово
age: 30              # обычный — однозначное число
created: "2024-05-04" # в кавычках — иначе распознается как дата
port: "8080"         # зависит от библиотеки — некоторые берут в кавычки числовые строки

Двойные кавычки оборачивают все строки в двойные кавычки. Это явно и аудитопригодно — любой читатель видит, что эти значения — строки, без рассуждений о спецификации. Цена — многословность и сниженная читаемость, особенно для глубоко вложенных конфигов.

# Режим double-quote
name: "Alice"
country: "NO"
replicas: "3"         # даже числа становятся строками — может вызывать ошибки схемы

Будьте осторожны: если целевая схема ожидает число, а вы сериализуете его как строку в кавычках, YAML-парсер корректно типизирует его как строку, но Kubernetes или другой строгий потребитель может отбросить поле как неверный тип.

Одинарные кавычки — только YAML-фича; в JSON синтаксиса одинарной кавычки нет. Одинарные кавычки литеральны: внутри них нет escape-последовательностей. Единственный спецслучай — одинарная кавычка внутри одинарной строки должна быть удвоена (''). Одинарные кавычки идеальны для строк с обратными слэшами или специальными символами, которые в двойных нужно было бы экранировать.

# Режим single-quote
pattern: 'C:\Users\alice\Documents'  # экранирование не нужно
regex: '\d+\.\d+'                    # обратные слэши литеральны

Для конвертаций JSON → YAML, рассчитанных на возврат в JSON, берите Auto или Double. Одинарные кавычки вносят YAML-специфичный синтаксис, требующий YAML-aware парсера на обратном пути.

Block-скаляры (| и >)

Block-скаляры YAML действительно полезны для многострочных строк — то, что JSON обрабатывает неуклюже через \n-escape.

Литеральный block-скаляр | сохраняет переводы строк точно:

# Литеральный блок — переводы сохраняются
script: |
  #!/bin/bash
  set -euo pipefail
  echo "Starting deployment"
  kubectl apply -f manifest.yaml

# Эквивалентное JSON-представление (нечитаемое)
# {"script": "#!/bin/bash\nset -euo pipefail\necho \"Starting deployment\"\nkubectl apply -f manifest.yaml\n"}

Свёрнутый block-скаляр > соединяет строки пробелами, превращая каждый перевод в пробел (за исключением пустых строк, которые становятся переводами):

# Свёрнутый блок — переводы становятся пробелами
description: >
  This service handles authentication
  for the entire platform. It supports
  OAuth2, SAML, and API key authentication.

# Результат: "This service handles authentication for the entire platform. It supports OAuth2, SAML, and API key authentication.\n"

Block-скаляры сияют для встраивания TLS-сертификатов, многострочных shell-скриптов или SQL-запросов в YAML-конфиги — там, где JSON-эквивалент был бы длинной экранированной однострочной строкой, не читаемой ни одним человеком.

При конвертации JSON в YAML большинство конвертеров (включая наш) использует режим Auto и представляет многострочные строки block-скалярами только при обнаружении переводов. Однострочные строки получают flow-скаляры (в кавычках или без). Применяйте наш конвертер JSON в YAML, чтобы увидеть вывод до коммита в манифест.

Отступы — 2 или 4 пробела, табы запрещены

Правила отступов YAML строже, чем кажутся. У спецификации одно абсолютное правило и одна конвенция, варьирующаяся от экосистемы к экосистеме.

Абсолютное правило: табы запрещены. Каждый уровень отступа — пробелы. Символ табуляции в YAML — ошибка парсинга в большинстве парсеров:

# НЕПРАВИЛЬНО — табы вызывают ошибки парсинга
apiVersion: apps/v1
kind: Deployment
metadata:
	name: my-app     # ← символ таба → ParseError

# ПРАВИЛЬНО — только пробелы
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app      # ← два пробела

Текст ошибки зависит от библиотеки. В PyYAML под Python:

yaml.scanner.ScannerError: while scanning for the next token
found character '\t' that cannot start any token

В Go yaml.v3:

yaml: line 4: found character that cannot start any token

Настройте редактор так, чтобы для YAML-файлов табы разворачивались в пробелы. В VS Code добавьте в settings рабочей области: "[yaml]": { "editor.insertSpaces": true, "editor.tabSize": 2 }.

Конвенция: 2 или 4 пробела. Оба варианта валидны. Конвенции экосистем различаются:

ЭкосистемаКонвенцияПричина
Манифесты Kubernetes2 пробелаОфициальные доки и примеры используют 2
Helm-чарты2 пробелаСледует конвенции K8s
Docker Compose2 пробелаПримеры в официальной спецификации
GitHub Actions2 пробелаПримеры в официальной документации
Ansible-плейбуки2 пробелаОфициальная документация
Традиционные конфиги4 пробелаСовпадает с дефолтом JSON-форматирования

Для всякого файла, который будет потреблять Kubernetes или Docker Compose, берите 2 пробела. Для standalone-конфигов, которые читают только люди и собственные инструменты, оба работают — лишь бы было согласованно внутри файла. Наш конвертер JSON в YAML по умолчанию ставит отступ 2 пробела и позволяет переключиться на 4 для проектов, предпочитающих такой стиль.

Ещё одно правило: дочерние элементы должны быть с большим отступом, чем родитель, но число дополнительных пробелов — любое положительное (1, 2, 3, 4…) — лишь бы внутри блока было согласованно. На практике всегда применяйте 2 или 4 для читаемости.

Обработка чисел в JSON ↔ YAML

Оба формата поддерживают числа, но граничные случаи различаются достаточно, чтобы вызывать production-баги.

Потеря точности на больших числах

Тип Number в JavaScript — 64-битный IEEE 754 float. Целые он представляет точно до 2^53 − 1 = 9 007 199 254 740 991. За этой границей точность теряется:

// Потеря точности в JavaScript — это не проблема YAML, но влияет на парсинг JSON
JSON.parse('{"v": 9007199254740993}').v
// → 9007199254740992   (3 стало 2 — потеряли один бит)

// Безопасно — в пределах 2^53
JSON.parse('{"v": 9007199254740991}').v
// → 9007199254740991   (точно)

Это важно для конвертации JSON в YAML в JavaScript-окружениях, потому что точность теряется ещё до начала YAML-сериализации. Поле metadata.resourceVersion Kubernetes — строковое именно потому, что resource version может выходить за safe integer. Другие поля, выглядящие как небольшие числа — observedGeneration, компоненты uid — безопаснее, но любое int64-поле в K8s-ответе потенциально под угрозой.

Обходные пути:

  • Применяйте Python или Go для конвейеров с большими числами — оба нативно работают с произвольными целыми.
  • В Node.js используйте JSON-парсер с поддержкой BigInt: JSON.parse(text, (_, v) => typeof v === 'number' && !Number.isSafeInteger(v) ? BigInt(v) : v).
  • Для полей, которые должны round-trip без потерь, сериализуйте их как строки в источнике.
  • При ревью сконвертированного YAML смотрите на поля вроде resourceVersion, generation и значения, выведенные из timestamp.

Странности восьмеричной и шестнадцатеричной нотации

YAML 1.1 трактует некоторые похожие на числа строки как недесятичные целые:

# Сюрпризы парсинга в YAML 1.1
permissions: 0755   # парсится как восьмеричное 493, не десятичное 755
value: 0x1A         # парсится как шестнадцатеричное 26, не строка "0x1A"

# Поведение YAML 1.2
permissions: 0755   # остаётся целым 755 (десятичным) — восьмеричное требует префикс 0o
permissions: 0o755  # парсится как восьмеричное 493 в обеих 1.1 и 1.2

# Безопасно для обеих спецификаций — берите в кавычки любое значение с ведущим нулём
permissions: "0755"  # всегда строка "0755"

Восьмеричная ловушка особенно опасна для Unix-прав на файлы, IP-адресов с ведущими нулями (на некоторых сетевых устройствах) и любых числовых кодов с ведущими нулями для выравнивания (ZIP-коды, коды товаров). Всегда берите такие значения в кавычки при ручном написании YAML или убедитесь, что ваш конвертер берёт их в кавычки — наш конвертер JSON в YAML распознаёт числовые строки из JSON и сохраняет строковый тип.

Реальные сценарии конвертации

Норвежская проблема и стратегии кавычек становятся конкретными, когда применяешь их к реальным сценариям.

Манифест Kubernetes из JSON

Канонический поток: kubectl get deploy my-app -o json даёт живой объект как JSON. Хочется почистить его (убрать status, creationTimestamp, managed-поля) и положить в git как YAML-манифест.

Источник JSON (сокращённо):

{
  "apiVersion": "apps/v1",
  "kind": "Deployment",
  "metadata": {
    "name": "my-app",
    "namespace": "production",
    "labels": {
      "app": "my-app",
      "region": "NO"
    }
  },
  "spec": {
    "replicas": 3,
    "selector": {
      "matchLabels": { "app": "my-app" }
    },
    "template": {
      "spec": {
        "containers": [{
          "name": "app",
          "image": "registry.example.com/my-app:v1.2.3",
          "env": [
            { "name": "REGION", "value": "NO" },
            { "name": "ENABLE_FEATURE", "value": "yes" }
          ]
        }]
      }
    }
  }
}

Ожидаемый YAML-вывод (с защитой от Norway):

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
  namespace: production
  labels:
    app: my-app
    region: "NO"          # в кавычках — норвежское слово
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
  template:
    spec:
      containers:
        - name: app
          image: registry.example.com/my-app:v1.2.3
          env:
            - name: REGION
              value: "NO"           # в кавычках — норвежское слово
            - name: ENABLE_FEATURE
              value: "yes"          # в кавычках — норвежское слово

Заметьте, что replicas: 3 без кавычек — это легитимное целое, которое Kubernetes ожидает как число. Норвежские слова в labels и значениях env — в кавычках. Наивный конвертер, не учитывающий boolean YAML 1.1, тихо выдал бы region: false и value: false.

После конвертации валидируйте: kubectl apply --dry-run=client -f manifest.yaml. Это ловит ошибки схемы без обращения к кластеру.

Попробуйте конвертацию в нашем конвертере JSON в YAML — вставьте JSON выше и увидите Norway-safe вывод мгновенно. Применяйте конвертер YAML в JSON, чтобы проверить round-trip.

Docker Compose из JSON

CI/CD-конвейеры иногда генерируют конфиги Docker Compose программно из JSON-хранилища настроек, а потом пишут их на диск как YAML, чтобы разработчики могли читать.

Критическая ловушка — restart-политика:

{"restart_policy": "no"}

В Compose restart_policy: "no" — валидное значение, означающее «никогда не перезапускать контейнер». Без кавычек в YAML это становится restart_policy: false, что Docker Compose может либо трактовать как ту же семантику (falsy = нет перезапуска), либо отбросить как ошибку валидации типа — поведение зависит от версии Compose. Кавычки обязательны.

Также следите за: Compose v3 deploy.restart_policy.condition: "on-failure" — значение on-failure содержит слово on, но оно через дефис и не входит в список триггеров, так что фактически безопасно. Однако condition: on (без -failure) рассогласуется. Берите значения переменных окружения в блоке environment: в кавычки, если они могут быть норвежскими словами.

Валидируйте Compose-файлы после конвертации: docker-compose config парсит и пере-выводит каноническую форму, всплывая ошибки типов.

Workflow GitHub Actions

Workflows GitHub Actions — YAML-файлы, которые редактируют вручную. Самый частый сценарий конвертации — чтение данных workflow из API GitHub (который возвращает JSON) и запись в локальный YAML-файл для редактирования.

Ключевые поля, за которыми надо следить:

# БЕЗОПАСНО — нет норвежских слов в стандартных GitHub Actions
on:                        # "on" здесь YAML-ключ, а не значение — обрабатывается иначе
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run tests
        run: |
          npm install
          npm test
        env:
          NODE_ENV: production   # безопасно — не норвежское слово
          DEBUG: "off"           # норвежское слово в значении — нужны кавычки

Заметка: on: как ключ YAML — особый случай — норвежская проблема применяется к значениям, а не ключам. Но on как значение (например, DEBUG: on) триггерит приведение типа. Блок env: заслуживает особого внимания, потому что значения переменных окружения — строки, но многие из них — короткие флаги, которые могут столкнуться с норвежскими словами.

Для workflows со спецификацией shell: валидные значения (bash, pwsh, sh, python) безопасны от Norway. Кастомные значения берите в кавычки превентивно.

Terraform JSON план → YAML

terraform show -json tfplan > plan.json выводит детальное JSON-представление того, что Terraform планирует создать, изменить или удалить. Конвертация в YAML делает его читаемее для ревью pull-request и аудита соответствия.

terraform plan -out=tfplan
terraform show -json tfplan > plan.json
# Затем конвертируем нашим инструментом или библиотекой

JSON плана Terraform сложен и глубок. Ключевые моменты при конвертации:

  1. Большие целые ID. ID облачных ресурсов (AWS account ID, GCP project number) и значения вычисляемых атрибутов могут быть большими числами. Конвертируйте через Python или Go, чтобы избежать потери точности float64.

  2. Строки ограничений версий. Terraform применяет ~>, >=, <= в ограничениях версий провайдеров. Это строковые значения, которые YAML обрабатывает корректно, пока они не норвежские слова — но ~> безопасно.

  3. Значения конфигурации провайдера. Вывод плана Terraform может включать значения конфигурации ресурсов. Если булево поле по дефолту false и представлено как "no" в схеме провайдера, это Norway-риск на обратном пути в YAML.

  4. Блок .sensitive_values. Чувствительные значения редактируются как true boolean в JSON плана. Они проходят конвертацию чисто, потому что true — не норвежское слово ни в одной версии YAML.

Конвертация Terraform → YAML — для ревью человеком, не для возврата в Terraform. Не применяйте YAML-манифесты как вход Terraform — родной формат Terraform — HCL, а его JSON-вход — специфичный и документирован отдельно.

Примеры кода — четыре языка

Node.js (eemeli/yaml + js-yaml)

Экосистема Node.js имеет две доминирующие YAML-библиотеки с заметно разной обработкой Norway:

// eemeli/yaml — рекомендуется, YAML 1.2 по умолчанию, Norway-safe
import { stringify } from 'yaml';
import { readFileSync } from 'fs';

const jsonInput = readFileSync('input.json', 'utf8');
const data = JSON.parse(jsonInput);

// Дефолт: YAML 1.2 — "NO" остаётся "NO", без приведения к boolean
const yamlOutput = stringify(data);
console.log(yamlOutput);
// region: NO      ← безопасно в 1.2, но для максимальной совместимости берите в кавычки явно

// Принудительная семантика YAML 1.1 (для окружений K8s/Helm, парсящих 1.1)
const yamlForK8s = stringify(data, { version: '1.1' });
// region: 'NO'    ← в кавычках, потому что 1.1 распарсил бы NO как false
console.log(yamlForK8s);
// js-yaml — широко распространён, но YAML 1.1, Norway-рискован без аккуратности
import yaml from 'js-yaml';
import { readFileSync } from 'fs';

const data = JSON.parse(readFileSync('input.json', 'utf8'));

// Дефолтный dump — норвежские слова могут быть без кавычек
const unsafe = yaml.dump(data);
// region: NO    ← распарсится как false, если перечитать парсером 1.1!

// Безопаснее: кастомная схема или принудительные кавычки
const safer = yaml.dump(data, {
  schema: yaml.JSON_SCHEMA,   // ограничивает JSON-совместимыми типами
  noCompatMode: false,
  lineWidth: -1,
  quotingType: '"',
  forceQuotes: false,         // берёт в кавычки только когда это нужно для JSON-схемы
});

Для новых проектов берите eemeli/yaml. Дефолт YAML 1.2 безопаснее, Document API даёт тонкий контроль над кавычками, и round-trip-точность обрабатывается лучше. Для проектов, уже использующих js-yaml, применяйте опцию JSON_SCHEMA, чтобы ограничить JSON-безопасными типами. Подробнее о фильтрации и преобразовании JSON перед конвертацией см. в шпаргалке по jq для паттернов предобработки.

Python (PyYAML + ruamel.yaml)

Python — доминирующий язык для инструментов Kubernetes, Ansible и data engineering — все они тяжело используют YAML.

import json
import yaml
import sys

# PyYAML — простой, стандартный, но YAML 1.1 по умолчанию
with open('input.json') as f:
    data = json.load(f)

output = yaml.dump(data, default_flow_style=False, allow_unicode=True)
# country: 'NO'   ← PyYAML на самом деле умеет авто-кавычить норвежские слова
# Но он НЕ берёт в кавычки "yes", "no" (нижний регистр) во всех конфигурациях:
# enabled: 'yes'   ← в кавычках
# tag: y           ← может быть в кавычках или нет в зависимости от версии

print(output)
import json
import sys
from ruamel.yaml import YAML

# ruamel.yaml — round-trip-точность, поддержка YAML 1.2, рекомендуется для production
yaml_rt = YAML()
yaml_rt.default_flow_style = False
yaml_rt.width = 4096              # предотвращает нежелательное переносы строк
yaml_rt.best_map_flow_style = False

with open('input.json') as f:
    data = json.load(f)

yaml_rt.dump(data, sys.stdout)
# Сохраняет порядок ключей, корректно обрабатывает Norway, поддерживает якоря на round-trip

Для скриптов автоматизации Ansible и Kubernetes, где вы конвертируете JSON-ответы API в YAML-манифесты, ruamel.yaml — безопаснее. PyYAML годится для простых скриптов, где вы контролируете входные данные и убедились, что норвежские слова не встречаются.

Если вы используете JSON5 или JSONC конфиги (с комментариями) перед конвертацией, сначала уберите расширения — см. руководство по JSON5 и JSONC для совместимых парсеров.

Go (gopkg.in/yaml.v3)

Go — язык самой экосистемы Kubernetes — kubectl, Helm, Argo, Flux и большинство K8s-операторов написаны на Go.

package main

import (
    "encoding/json"
    "fmt"
    "os"

    "gopkg.in/yaml.v3"
)

func main() {
    // Читаем JSON-вход
    jsonBytes, err := os.ReadFile("input.json")
    if err != nil {
        panic(err)
    }

    // Unmarshal JSON в общую map
    var data map[string]interface{}
    if err := json.Unmarshal(jsonBytes, &data); err != nil {
        panic(err)
    }

    // Marshal в YAML — yaml.v3 использует семантику YAML 1.2
    yamlBytes, err := yaml.Marshal(data)
    if err != nil {
        panic(err)
    }

    fmt.Println(string(yamlBytes))
    // country: "NO"     ← yaml.v3 корректно берёт норвежские слова в кавычки
    // replicas: 3       ← целые остаются целыми
    // enabled: true     ← boolean остаются boolean
}

yaml.v3 — заметное улучшение над yaml.v2 по Norway-безопасности. Библиотека v2 следовала YAML 1.1 и писала NO без кавычек; v3 корректно берёт в кавычки неоднозначные значения. Если вы поддерживаете старый Go-проект на v2, обновитесь до v3 — API в основном совместимо, и улучшение безопасности оправдывает миграцию.

Для типобезопасной конвертации со структурами Go (вместо map[string]interface{}) используйте struct-теги:

type DeploymentLabels struct {
    App    string `yaml:"app" json:"app"`
    Region string `yaml:"region" json:"region"`
}
// yaml.Marshal на структуре с полем "NO" корректно возьмёт его в кавычки в v3

Bash CLI (yq + jq)

Для shell-скриптов и быстрой разовой конвертации yq (версия Mike Farah, mikefarah/yq) переводит JSON в YAML одной командой:

# Установка yq
brew install yq                         # macOS
sudo wget -qO /usr/local/bin/yq \
  https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64
chmod +x /usr/local/bin/yq              # Linux

# Конвертируем JSON-файл в YAML
yq -P < input.json > output.yaml

# Конвертируем из вывода kubectl JSON
kubectl get deploy my-app -o json | yq -P > manifest.yaml

# Прокидываем через jq для фильтрации, затем переводим в YAML
kubectl get deploy my-app -o json \
  | jq 'del(.status, .metadata.creationTimestamp, .metadata.managedFields)' \
  | yq -P > clean-manifest.yaml

Конвейер jq | yq — мощный паттерн: применяйте jq для манипуляций с JSON (фильтрация полей, изменение структуры, запросы значений), а yq -P — как финальный YAML-сериализатор. Паттерны jq см. в шпаргалке по jq — там 30 рабочих паттернов, включая интеграции с kubectl и aws.

Norway-осторожность с yq: yq (mikefarah) уважает входной тип из JSON — строка "NO" во входном JSON сериализуется как YAML-строка с кавычками. Но если вы генерируете YAML напрямую через yq (не из JSON-входа), значения с норвежскими словами надо брать в кавычки явно. Применяйте наш конвертер YAML в JSON, чтобы проверить round-trip после вывода yq.

Граничные случаи и подводные камни

Помимо норвежской проблемы, конвертация JSON ↔ YAML имеет несколько граничных случаев, на которых спотыкаются опытные инженеры:

  1. Многодокументный YAML (разделитель ---). Один YAML-файл может содержать несколько документов, разделённых ---. В JSON эквивалентного понятия нет. При конвертации многодокументного YAML в JSON большинство инструментов либо берёт только первый документ, либо склеивает все документы в массив, либо падает с ошибкой. При конвертации JSON в YAML по конвенции добавляется один заголовок ---. Решите и задокументируйте поведение явно для конвейеров, где может встретиться многодокументный файл.

  2. Якоря и алиасы YAML. YAML поддерживает определения &anchor и ссылки *alias для DRY-конфигов. При конвертации YAML в JSON якоря должны быть развёрнуты — итоговый JSON может оказаться сильно больше исходного YAML. При конвертации JSON в YAML конвертер не может реконструировать якоря, которых не было в исходнике. Алиасы — фича только YAML.

  3. Неявный парсинг timestamp. Парсеры YAML 1.1 переводят 2024-05-04 и 2024-05-04T12:00:00Z в нативные объекты дат, а не в строки. Когда такой объект сериализуется обратно в JSON, вывод зависит от библиотеки: одни выдают ISO-строки, другие — Unix timestamp, третьи — null. Round-trip дат через YAML без явных строковых кавычек ("2024-05-04") может тихо изменить формат.

  4. Тег !!binary. YAML может встраивать base64-кодированные бинарные данные с тегом !!binary. У JSON нет бинарного типа — бинарь должен быть base64-строкой. При конвертации YAML с полями !!binary в JSON декодируйте до base64-строки. При конвертации обратно реконструировать бинарный тег без знания схемы нельзя. Kubernetes использует !!binary для некоторых значений секретов.

  5. Коллизии типов ключей. JSON требует, чтобы ключи объектов были строками. YAML позволяет ключи любого типа — целые, boolean, даже сложные объектные ключи. YAML-файл с true: value или 1: value нельзя точно представить в JSON. Большинство конвертеров стрингифицирует ключи, но семантика меняется.

  6. Вариативность представления null. В YAML null, ~, Null, NULL и пустое значение все означают null. В JSON null только null. При конвертации YAML в JSON всё это нормализуется в null. Но при обратной конвертации представление null имеет значение — ~ компактнее, null явнее. Выберите одно и придерживайтесь.

  7. Изменение порядка сортировки. У JSON-объектов формально порядок ключей не определён (хотя большинство парсеров сохраняет порядок вставки). В YAML-mapping тоже нет требуемого порядка. Но некоторые YAML-библиотеки сортируют ключи по алфавиту по умолчанию. Это вызывает большие диффы в системе контроля версий, если исходный JSON шёл в другом порядке. Включайте sort_keys=False в PyYAML (default_flow_style=False сам по себе сортировку не отключает) и эквивалентные опции в других библиотеках.

Когда НЕ конвертировать

Конвертация — не всегда правильный ответ. Сценарии, в которых лучше остаться в исходном формате:

Не конвертируйте YAML в JSON, если YAML содержит комментарии, документирующие бизнес-логику. Комментарии YAML — не часть модели данных; в любой сериализации в JSON они исчезают. Если в манифесте Kubernetes есть комментарии, объясняющие, почему выбран конкретный лимит ресурсов или почему сделано исключение из политики безопасности, конвертация в JSON уничтожит эту документацию. Оставьте YAML.

Не авто-конвертируйте конфиги в CI без round-trip-тестов. Если ваш конвейер конвертирует JSON в YAML и потом применяет YAML к кластеру, добавьте шаг round-trip: YAML обратно в JSON и сравнение с оригиналом. Это ловит сюрпризы приведения типов до продакшена.

Не конвертируйте только потому, что инструмент выдаёт JSON. kubectl, aws, terraform и docker inspect все выдают JSON, но большинство этих инструментов также принимает YAML на вход. Перед тем как строить шаг конвертации, проверьте, может ли целевой инструмент принять YAML напрямую — большинство современных DevOps-инструментов могут. Наш конвертер YAML в JSON особенно полезен, когда нужен именно JSON для инструмента, не принимающего YAML.

Не конвертируйте, если схемы различаются. Если ваш JSON использует ключи в camelCase, а потребитель YAML ожидает snake_case (или наоборот), помимо конвертации формата нужен шаг трансформации. Голая конвертация формата выдаст синтаксически корректный, но семантически неправильный YAML. Решите задачу маппинга схем явно.

Не держите оба формата синхронизированными вручную. Если вы поддерживаете config.json и config.yaml, которые должны быть эквивалентными, они разойдутся. Выберите один канонический формат и выводите второй автоматически — а лучше выберите один формат и устраните дублирование.

Часто задаваемые вопросы

Норвежская проблема YAML всё ещё затрагивает современные системы?

Да — она пронизывает экосистему. Kubernetes и Helm применяют библиотеку Go yaml.v2 (семантика YAML 1.1) в значимых частях кодовой базы. Ansible применяет PyYAML (YAML 1.1). Workflows GitHub Actions разбираются внутренним YAML-парсером GitHub со своим поведением. Большинство YAML-файлов CI/CD «в дикой природе» обрабатываются парсерами YAML 1.1. Предполагайте семантику 1.1, пока не проверите обратное.

Зачем конвертировать JSON в YAML, если YAML сложнее разбирать?

Конвертация не про сложность парсера — про человеческое редактирование. JSON идеален для машин; YAML идеален для людей, которым нужно читать, править и ревьюить конфиги. Манифест Kubernetes, лежащий в git, проходящий ревью pull-request и точно настраиваемый инженерами, должен быть YAML. Тот же манифест, получаемый из API для программной обработки, должен быть JSON. Наш конвертер JSON в YAML наводит мост между двумя.

Можно ли round-trip JSON ↔ YAML без потерь?

С оговорками — да, для JSON-совместимых данных. JSON — подмножество YAML 1.2, поэтому всякий валидный JSON-документ — валидный YAML 1.2. Проход JSON → YAML → JSON должен быть без потерь для любых данных без неявного приведения типов. Норвежская проблема означает, что строка JSON "NO" переживёт прямой проход, только если конвертер возьмёт её в кавычки, и переживёт обратный, только если YAML-парсер уважит кавычки. Применяйте библиотеку YAML 1.2 в обоих направлениях для гарантии lossless round-trip.

Какая YAML-библиотека самая безопасная для production?

Для Python: ruamel.yaml, настроенный на YAML 1.2. Для Node.js: eemeli/yaml (пакет yaml на npm). Для Go: gopkg.in/yaml.v3. Все три реализуют семантику YAML 1.2 или имеют явные режимы 1.2 и корректно обрабатывают норвежские слова. Избегайте библиотек YAML 1.1 в новых проектах. Если приходится использовать 1.1-библиотеку (PyYAML, js-yaml, yaml.v2) ради совместимости, всегда явно берите Norway-склонные строки в кавычки.

Поддерживает ли манифест Kubernetes комментарии после JSON-конвертации?

Нет — комментарии нельзя восстановить из JSON. У JSON нет синтаксиса комментариев, поэтому конвертировать нечего. Когда вы запускаете kubectl get deploy -o json и конвертируете вывод в YAML для git, в итоговом YAML комментариев нет. Комментарии в манифесте Kubernetes должны быть написаны человеком после конвертации. Это одна из причин, почему держать рукописный YAML каноническим источником часто предпочтительнее round-trip через JSON-API.

Как обрабатывать большие целые вроде resourceVersion или наносекундных timestamp?

Поле Kubernetes metadata.resourceVersion — строковое намеренно — команда Kubernetes знала, что JSON-парсеры в JavaScript и других float64-средах теряют точность на больших целых. Всегда трактуйте его как строку. Для действительно числовых больших целых (например, наносекундные epoch-timestamp в некоторых tracing-системах) применяйте Python int, Go int64 или Node.js BigInt. Никогда не пропускайте через JSON.parse() в JavaScript без custom reviver-функции. При конвертации в YAML такие большие целые безопасны — у YAML нет лимита точности для целых. Опасность — на обратном пути через JSON-парсер JavaScript.

YAML 1.2 уже широко принят?

Неравномерно. Главные языковые библиотеки мигрировали: Go yaml.v3, Python ruamel.yaml и Node.js eemeli/yaml поддерживают или используют YAML 1.2 по умолчанию. Но Kubernetes, Ansible и значительная часть DevOps-экосистемы по-прежнему работает на парсерах YAML 1.1 из-за стоимости миграции по обратной совместимости. Принятие YAML 1.2 в новых проектах рекомендуется, но предполагайте 1.1 для всякой системы, которую вы сами не настроили.

Стандартизировать на JSON или YAML для конфигов?

Стандартизируйте по назначению, не по формату. Применяйте JSON для конфигов, потребляемых кодом (тела API-запросов, файлы конфигов SDK, программный инструментарий). Применяйте YAML для конфигов, потребляемых людьми (манифесты Kubernetes, CI-конвейеры, конфиги деплоя, Ansible-плейбуки). Избегайте смешения для одного типа конфига — выберите одно представление на тип и автоматизируйте конвертацию, если нужны оба. Когда конвертация нужна, и наш конвертер JSON в YAML, и YAML в JSON работают полностью в браузере — данные не покидают устройство.

Попробуйте сейчас

Готовы конвертировать реальный файл? Попробуйте наш конвертер JSON в YAML, чтобы санировать JSON в безопасный Kubernetes-YAML — он автоматически берёт в кавычки норвежские слова (NO, yes, on, off и весь список boolean YAML 1.1) и позволяет выбрать отступ 2 или 4 пробела. Для обратного направления наш конвертер YAML в JSON справляется с якорями, алиасами и многодокументным YAML. Оба инструмента работают полностью в браузере — данные не покидают устройство, что важно при работе с production-манифестами Kubernetes или планами Terraform с чувствительными конфигурациями ресурсов.

Похожие статьи

Все статьи