Межі конвертації 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 у flow-стилі. Зворотне не так.
| Можливість | JSON | YAML 1.2 |
|---|---|---|
| Коментарі | ✗ | ✓ |
| Якорі та псевдоніми | ✗ | ✓ |
Кілька документів (---) |
✗ | ✓ |
| Ключі не-рядки | ✗ | ✓ (1: a, true: b) |
| Складні ключі | ✗ | ✓ (? [a, b]) |
| Чутливість до відступів | ✗ | ✓ (відступи табуляцією відхиляються) |
| Виведення типів | явне | неявне (0o17, 0x1F, .inf) |
| Читабельність в один рядок | добра | погана (довгий вміст покладається на синтаксис згортання) |
1.1 Виведення типів — найбільша пастка
YAML розбирає деякі значення, схожі на рядки, в інші типи:
zip: 01234 # рядок чи вісімкове ціле? базова схема YAML 1.2 трактує його як рядок
version: 1.10 # 1.1 (дробове); семантика втрачена
country: NO # рядок у YAML 1.2; булеве false у YAML 1.1
date: 2026-10-30 # деякі реалізації розбирають його як об'єкт дати
Приклад із 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, // повертати масив навіть для одного елемента
trim: true,
explicitRoot: false,
});
2.4 Нагадування про безпеку
Перед розбором недовіреного XML ви маєте вимкнути зовнішні сутності, інакше можете запустити XXE (читання локальних файлів, зондування внутрішніх мереж):
// В екосистемі Node DTD і зовнішні сутності зазвичай вимикаються через опції парсера
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, // вимкнути автоматичне перенесення, щоб довгі рядки не розбивалися
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 записана, і команда спільно використовує один набір параметрів;
- [ ] DTD і зовнішні сутності вимкнені під час розбору недовіреного XML;
- [ ] До і після конвертації порівнюються кількість байтів і полів, і будь-яка аномалія піднімає тривогу.
Часті питання
Як обрати напрямок конвертації між JSON, YAML і XML?
Надавайте перевагу JSON як проміжному формату: спершу розберіть YAML і XML у модель даних, а потім серіалізуйте з цієї моделі, а не переписуйте текст. Використовуйте Document API YAML, коли коментарі мають зберегтися, і зафіксуйте одну конвенцію відображення XML, перш ніж знадобляться атрибути чи простори імен.
Чому конвертація XML у JSON втрачає атрибути й простори імен?
У JSON немає поняття атрибутів чи просторів імен, тож атрибути XML, префікси та URI просторів імен доводиться згортати у звичайні ключі, зазвичай із префіксом @ або вузлом #text. Щойно правила відображення розходяться, той самий XML дає різний JSON і не може пройти зворотне перетворення.
Чи варто підтримувати довготривалу двосторонню синхронізацію JSON, YAML і XML?
Ні. Коментарі, якорі, атрибути й простори імен XML зазнають втрат між форматами, а двостороння синхронізація постійно накопичує розбіжності. Оберіть один вихідний формат, а інші трактуйте як одноразові експорти або тимчасові подання.