The Boundaries of JSON ↔ YAML and XML Conversion
Bottom line: The claim that JSON is a subset of YAML only guarantees one direction, and XML has no official JSON mapping at all — everything rests on convention.

Bottom line: use JSON as the intermediate format: parse the source into a data model, then serialize to the target; before converting, decide whether comments, XML attributes and namespaces, and numeric or boolean types must survive.
Whether a conversion pipeline is usable does not depend on how pretty the output looks, but on whether you are aware of how much it threw away.
1. JSON and YAML: one-way containment
The YAML 1.2 specification states it plainly: JSON is a subset of YAML. That is, any valid JSON is valid YAML:
{"name": "json", "tags": ["a", "b"], "count": 3}
This snippet is both JSON and YAML flow style. The reverse does not hold.
| Feature | JSON | YAML 1.2 |
|---|---|---|
| Comments | ✗ | ✓ |
| Anchors and aliases | ✗ | ✓ |
Multiple documents (---) |
✗ | ✓ |
| Non-string keys | ✗ | ✓ (1: a, true: b) |
| Complex keys | ✗ | ✓ (? [a, b]) |
| Indentation sensitive | ✗ | ✓ (tab indentation is rejected) |
| Type inference | explicit | implicit (0o17, 0x1F, .inf) |
| Single-line readability | good | poor (long content relies on folding syntax) |
1.1 Type inference is the biggest trap
YAML parses some values that look like strings into other types:
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
The NO example has burned plenty of people: with a YAML 1.1 parser (some older libraries), country: NO becomes false.
Countermeasure: quote every identifier that "looks like a number or a boolean", as in version: "1.10".
1.2 Anchors, aliases, and multiple documents
defaults: &defaults
retries: 3
timeout: 30
serviceA:
<<: *defaults
host: a.example.com
---
serviceB:
<<: *defaults
host: b.example.com
After expanding the anchors and converting to JSON you typically end up with two duplicated configuration objects, or with one array (multiple documents must become [{...}, {...}]).
Either way, anchors are not restored on the reverse conversion — that loss is unavoidable.
1.3 Conversion that preserves comments
Most libraries simply drop comments during conversion. When you need to keep them, use the Document API of the yaml package:
import { parseDocument } from 'yaml';
const document = parseDocument(`
# Service configuration
port: 8080
`);
document.set('port', 3000);
console.log(document.toString());
// # Service configuration
// port: 3000
The test is simple: if this file will be edited by humans over and over, do not take the "parse → object → serialize" path.
1.4 How to verify round-trip consistency
/**
* 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);
}
Running it against real data is worth more than reading the docs ten times. Make sure your sample set covers: null, the empty string, the empty array, "00", "1e5", negative zero, integers larger than 2^53, and long strings containing newlines.
2. JSON and XML: there is no standard mapping
XML predates JSON by a long margin and has far stronger structural capabilities (attributes, namespaces, mixed content, DTD). Between the two there is no official mapping specification; every tool does the same thing: pick a convention, then try not to lose much.
2.1 Common conventions
| Convention | Representation | Example |
|---|---|---|
| Element → object key | A same-named element becomes a field | <a>1</a> → {"a": "1"} |
Attribute → @ prefix |
@name |
<a id="1"/> → {"a": {"@id": "1"}} |
Text → #text |
When there is both an attribute and text | <a id="1">hi</a> → {"a": {"@id": "1", "#text": "hi"}} |
| Repeated element → array | Two or more occurrences become an array | <i>1</i><i>2</i> → {"i": ["1","2"]} |
| Namespace → prefix | 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 Five unavoidable losses
- Element order: JSON objects are unordered, while XML is an ordered sequence of elements;
- Attributes vs child elements: they carry different semantics in XML, but both become plain fields in JSON;
- Mixed content: structures like
<p>你好 <b>世界</b> 再见</p>cannot be expressed with a plain object; - Namespace declarations:
xmlnsis a scoping concept; once flattened into fields, the scope is gone; - CDATA and entities:
<![CDATA[...]]>and&are nothing more than text in JSON.
2.3 The dilemma of "one element or an array"
<list><item>1</item></list>
With a single item, xml2js returns an object by default and only returns an array once there are two.
As a result, front-end code has to write Array.isArray(x) ? x : [x].
Countermeasure: turn on the "always array" option when parsing, or declare the field as an array type based on a schema.
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 Security reminder
Before parsing untrusted XML you must disable external entities, otherwise you may trigger XXE (reading local files, probing internal networks):
// In the Node ecosystem, DTDs and external entities are usually disabled through parser options
const parserOptions = { doctype: false, externalEntities: false };
XML → JSON is only the first layer of conversion; converting does not eliminate the attack surface that XML brings.
3. Where each of the three formats fits
| Scenario | Recommended format | Reason |
|---|---|---|
| Public HTTP APIs | JSON | Natively supported by browsers and every language |
| Hand-maintained configuration | YAML | Comments, indentation, readability |
| Documents that need a strong schema and namespaces (SOAP, Office Open XML, SVG) | XML | Structural capabilities and the existing ecosystem |
| Interaction with operational tooling (K8s, CI) | YAML | The ecosystem default |
| Persistence, caches, logs | JSON / JSONL | Universal and dependency-free |
4. Implementation notes
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',
});
}
A few practical options:
lineWidth: 0turns off automatic wrapping; otherwise long URLs get split across lines and become hard to read;- quoting everything with double quotes prevents values like
NOand1.10from being misread; - for data with sensitive numeric precision, convert to strings before serializing.
For the XML direction, fix a single mapping function and put the "attribute prefix", "text key name", and "array strategy" into its signature instead of scattering them around:
type XmlMappingOptions = {
attributePrefix: string;
textKey: string;
alwaysArray: boolean;
};
const defaultMapping: XmlMappingOptions = {
attributePrefix: '@',
textKey: '#text',
alwaysArray: false,
};
5. Checklist
- [ ] Every "number/boolean-like" identifier in YAML is quoted;
- [ ] A validation sample set for JSON → YAML → JSON is defined and covered by tests;
- [ ] When comments must be preserved, the Document API is used instead of an object round trip;
- [ ] The XML mapping convention is written down and the team shares one set of parameters;
- [ ] DTDs and external entities are disabled when parsing untrusted XML;
- [ ] Byte counts and field counts are compared before and after conversion, and any anomaly raises an alert.
FAQ
How should I choose the conversion direction between JSON, YAML and XML?
Prefer JSON as the intermediate format: parse YAML and XML into a data model first, then serialize from that model instead of rewriting text. Use the YAML Document API when comments must survive, and fix one XML mapping convention before you need attributes or namespaces.
Why does XML-to-JSON conversion lose attributes and namespaces?
JSON has no concept of attributes or namespaces, so XML attributes, prefixes and namespace URIs must be folded into ordinary keys, commonly with an @ prefix or a #text node. Once mapping rules diverge, the same XML produces different JSON and cannot round-trip.
Should I keep JSON, YAML and XML in long-term two-way sync?
No. Comments, anchors, XML attributes and namespaces are lossy across formats, and two-way sync keeps accumulating differences. Pick one source format and treat the others as one-off exports or temporary views.