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 у 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 П'ять неминучих втрат

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