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

Коротко: используйте 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 Пять неизбежных потерь
- Порядок элементов: объекты JSON неупорядочены, а XML — упорядоченная последовательность элементов;
- Атрибуты против дочерних элементов: в XML они несут разную семантику, а в JSON оба становятся обычными полями;
- Смешанное содержимое: структуры вроде
<p>你好 <b>世界</b> 再见</p>нельзя выразить простым объектом; - Объявления пространств имён:
xmlns— это понятие области видимости; после уплощения в поля область исчезает; - CDATA и сущности:
<![CDATA[...]]>и&в 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 и пространства имён теряются при переходе между форматами, а двусторонняя синхронизация накапливает расхождения. Выберите один исходный формат, а остальные считайте разовой выгрузкой или временным представлением.