JSON Toolkit

Границы преобразования JSON ↔ YAML и XML

Главное: Утверждение, что JSON — подмножество YAML, гарантирует только одно направление, а у XML вообще нет официального отображения в JSON — всё держится на соглашениях.

Средний~12 мин чтенияОпубликовано 2026-10-30Обновлено 2026-10-30
Обложка статьи о границах преобразования JSON ↔ YAML и XML

Коротко: используйте JSON как промежуточный формат: разберите источник в модель данных, а затем сериализуйте в целевой формат; до преобразования решите, должны ли сохраниться комментарии, атрибуты и пространства имён XML, а также числовые и логические типы.

Пригодность конвейера преобразования зависит не от того, насколько красиво выглядит вывод, а от того, осознаёте ли вы, сколько он выбросил.

1. JSON и YAML: одностороннее вложение

Спецификация YAML 1.2 говорит прямо: JSON является подмножеством YAML. То есть любой допустимый JSON — допустимый YAML:

{"name": "json", "tags": ["a", "b"], "count": 3}

Этот фрагмент одновременно JSON и YAML в потоковом стиле. Обратное неверно.

Возможность JSON YAML 1.2
Комментарии ✗ ✓
Якоря и псевдонимы ✗ ✓
Несколько документов (---) ✗ ✓
Ключи не-строки ✗ ✓ (1: a, true: b)
Сложные ключи ✗ ✓ (? [a, b])
Чувствительность к отступам ✗ ✓ (табуляция в отступах отвергается)
Вывод типов явный неявный (0o17, 0x1F, .inf)
Читаемость одной строкой хорошая плохая (длинное содержимое зависит от свёрнутого синтаксиса)

1.1 Вывод типов — главная ловушка

YAML разбирает некоторые значения, похожие на строки, в другие типы:

zip: 01234        # string or octal integer? the YAML 1.2 core schema treats it as a string
version: 1.10     # 1.1 (a float); the semantics are lost
country: NO       # a string in YAML 1.2; boolean false in YAML 1.1
date: 2026-10-30  # some implementations parse it as a date object

Пример с NO обжёг многих: с парсером YAML 1.1 (в некоторых старых библиотеках) country: NO превращается в false. Мера противодействия: заключайте в кавычки каждый идентификатор, который «выглядит как число или логическое значение», например version: "1.10".

1.2 Якоря, псевдонимы и несколько документов

defaults: &defaults
  retries: 3
  timeout: 30

serviceA:
  <<: *defaults
  host: a.example.com

---
serviceB:
  <<: *defaults
  host: b.example.com

После раскрытия якорей и преобразования в JSON обычно получаются два дублирующихся объекта конфигурации или один массив (несколько документов должны стать [{...}, {...}]). В любом случае якоря не восстанавливаются при обратном преобразовании — эта потеря неизбежна.

1.3 Преобразование с сохранением комментариев

Большинство библиотек просто отбрасывает комментарии при преобразовании. Когда их нужно сохранить, используйте Document API пакета yaml:

import { parseDocument } from 'yaml';

const document = parseDocument(`
# Service configuration
port: 8080
`);

document.set('port', 3000);
console.log(document.toString());
// # Service configuration
// port: 3000

Проверка проста: если этот файл будут снова и снова править люди, не идите путём «разбор → объект → сериализация».

1.4 Как проверить согласованность туда-обратно

/**
 * Verifies round-trip consistency for JSON → YAML → JSON.
 * @param sourceValue Original JSON value
 * @param stringify YAML serialization function
 * @param parse YAML parsing function
 * @returns Whether the two results are exactly equal
 */
function isRoundTripStable(
  sourceValue: unknown,
  stringify: (input: unknown) => string,
  parse: (input: string) => unknown,
): boolean {
  const yamlText = stringify(sourceValue);
  const restored = parse(yamlText);
  return JSON.stringify(restored) === JSON.stringify(sourceValue);
}

Прогнать её на реальных данных полезнее, чем десять раз перечитать документацию. Убедитесь, что набор примеров покрывает: null, пустую строку, пустой массив, "00", "1e5", отрицательный ноль, целые больше 2^53 и длинные строки с переводами строк.

2. JSON и XML: стандартного отображения нет

XML появился намного раньше JSON и обладает гораздо более сильными структурными возможностями (атрибуты, пространства имён, смешанное содержимое, DTD). Между ними нет официальной спецификации отображения; каждый инструмент делает одно и то же: выбирает соглашение и пытается потерять не слишком много.

2.1 Распространённые соглашения

Соглашение Представление Пример
Элемент → ключ объекта Одноимённый элемент становится полем <a>1</a> → {"a": "1"}
Атрибут → префикс @ @name <a id="1"/> → {"a": {"@id": "1"}}
Текст → #text Когда есть и атрибут, и текст <a id="1">hi</a> → {"a": {"@id": "1", "#text": "hi"}}
Повторяющийся элемент → массив Два и более вхождения становятся массивом <i>1</i><i>2</i> → {"i": ["1","2"]}
Пространство имён → префикс ns:tag <ns:tag/> → {"ns:tag": ...}
<order id="A1001">
  <item sku="X1" qty="2">笔记本</item>
  <item sku="X2" qty="1">钢笔</item>
</order>
{
  "order": {
    "@id": "A1001",
    "item": [
      { "@sku": "X1", "@qty": "2", "#text": "笔记本" },
      { "@sku": "X2", "@qty": "1", "#text": "钢笔" }
    ]
  }
}

2.2 Пять неизбежных потерь

  1. Порядок элементов: объекты JSON неупорядочены, а XML — упорядоченная последовательность элементов;
  2. Атрибуты против дочерних элементов: в XML они несут разную семантику, а в JSON оба становятся обычными полями;
  3. Смешанное содержимое: структуры вроде <p>你好 <b>世界</b> 再见</p> нельзя выразить простым объектом;
  4. Объявления пространств имён: xmlns — это понятие области видимости; после уплощения в поля область исчезает;
  5. CDATA и сущности: <![CDATA[...]]> и &amp; в JSON — не более чем текст.

2.3 Дилемма «один элемент или массив»

<list><item>1</item></list>

При одном item xml2js по умолчанию возвращает объект и возвращает массив, только когда элементов два. В результате фронтенд-коду приходится писать Array.isArray(x) ? x : [x].

Мера противодействия: включите опцию «всегда массив» при разборе или объявите поле типом-массивом на основе схемы.

import { parseStringPromise } from 'xml2js';

const parsed = await parseStringPromise(xmlText, {
  explicitArray: true,   // return an array even for a single element
  trim: true,
  explicitRoot: false,
});

2.4 Напоминание о безопасности

Прежде чем разбирать недоверенный XML, обязательно отключите внешние сущности, иначе можно вызвать XXE (чтение локальных файлов, зондирование внутренней сети):

// In the Node ecosystem, DTDs and external entities are usually disabled through parser options
const parserOptions = { doctype: false, externalEntities: false };

XML → JSON — лишь первый слой преобразования; преобразование не устраняет поверхность атаки, которую приносит XML.

3. Где место каждому из трёх форматов

Сценарий Рекомендуемый формат Причина
Публичные HTTP API JSON Нативно поддерживается браузерами и каждым языком
Конфигурация, поддерживаемая вручную YAML Комментарии, отступы, читаемость
Документы, которым нужны строгая схема и пространства имён (SOAP, Office Open XML, SVG) XML Структурные возможности и существующая экосистема
Взаимодействие с эксплуатационным инструментарием (K8s, CI) YAML Выбор экосистемы по умолчанию
Хранение, кеши, логи JSON / JSONL Универсально и без зависимостей

4. Замечания по реализации

import { stringify as toYaml } from 'yaml';

/**
 * Converts a JSON value to YAML text while keeping Chinese characters unescaped.
 * @param value The JSON value to convert
 * @returns The YAML text
 */
function jsonToYaml(value: unknown): string {
  return toYaml(value, {
    indent: 2,
    lineWidth: 0,      // disable automatic wrapping so long strings are not split
    defaultStringType: 'QUOTE_DOUBLE',
  });
}

Несколько практичных опций:

  • lineWidth: 0 отключает автоматический перенос; иначе длинные URL разрываются по строкам и становятся плохо читаемыми;
  • заключение всего в двойные кавычки не даёт значениям вроде NO и 1.10 быть прочитанными неправильно;
  • для данных с чувствительной числовой точностью преобразуйте значения в строки до сериализации.

Для направления XML зафиксируйте одну функцию отображения и вынесите «префикс атрибута», «имя текстового ключа» и «стратегию массивов» в её сигнатуру, а не разбрасывайте их по коду:

type XmlMappingOptions = {
  attributePrefix: string;
  textKey: string;
  alwaysArray: boolean;
};

const defaultMapping: XmlMappingOptions = {
  attributePrefix: '@',
  textKey: '#text',
  alwaysArray: false,
};

5. Чек-лист

  • [ ] Каждый «похожий на число/логическое значение» идентификатор в YAML заключён в кавычки;
  • [ ] Набор проверочных примеров для JSON → YAML → JSON определён и покрыт тестами;
  • [ ] Когда комментарии нужно сохранить, используется Document API, а не круговой обход через объект;
  • [ ] Соглашение отображения XML записано, и команда использует один набор параметров;
  • [ ] При разборе недоверенного XML отключены DTD и внешние сущности;
  • [ ] Количество байтов и полей сравнивается до и после преобразования, и любая аномалия поднимает тревогу.

Вопросы и ответы

Как выбрать направление преобразования между JSON, YAML и XML?

Предпочитайте JSON как промежуточный формат: сначала разберите YAML и XML в модель данных, а затем сериализуйте из этой модели, а не переписывайте текст. Используйте Document API YAML, когда комментарии должны сохраниться, и фиксируйте одно соглашение отображения XML, прежде чем понадобятся атрибуты или пространства имён.

Почему при преобразовании XML в JSON теряются атрибуты и пространства имён?

В JSON нет понятий атрибута и пространства имён, поэтому атрибуты XML, префиксы и URI пространств имён приходится складывать в обычные ключи, обычно с префиксом @ или узлом #text. Когда правила отображения расходятся, один и тот же XML даёт разный JSON и не проходит круговое преобразование.

Стоит ли держать JSON, YAML и XML в долгосрочной двусторонней синхронизации?

Нет. Комментарии, якоря, атрибуты XML и пространства имён теряются при переходе между форматами, а двусторонняя синхронизация накапливает расхождения. Выберите один исходный формат, а остальные считайте разовой выгрузкой или временным представлением.