JSON Toolkit

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.

Intermediate~12 min readPublished 2026-10-30Updated 2026-10-30
Cover for the boundaries of JSON ↔ YAML and XML conversion

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

  1. Element order: JSON objects are unordered, while XML is an ordered sequence of elements;
  2. Attributes vs child elements: they carry different semantics in XML, but both become plain fields in JSON;
  3. Mixed content: structures like <p>你好 <b>世界</b> 再见</p> cannot be expressed with a plain object;
  4. Namespace declarations: xmlns is a scoping concept; once flattened into fields, the scope is gone;
  5. CDATA and entities: <![CDATA[...]]> and &amp; 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: 0 turns off automatic wrapping; otherwise long URLs get split across lines and become hard to read;
  • quoting everything with double quotes prevents values like NO and 1.10 from 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.

References

Related articles

Converting JSON to and from CSV and Tabular Data

CSV is two-dimensional, JSON is tree-shaped. The hard part of conversion is not the code — it is deciding up front how arrays get flattened and how types are preserved.

Beginner~10 min readUpdated 2026-10-30

Managing JSON for Configuration, Fixtures, and Local Data

Configuration must be layered and validated; fixtures must be deterministic, readable, and diffable. If these two kinds of files are managed poorly, troubleshooting costs stay high for a long time.

Beginner~10 min readUpdated 2026-10-30

JSON to YAML on the Command Line: Four Traps Behind One yq Command

The most-copied command in every tutorial, yq -P input.json > output.yaml, still emits JSON on yq v4. This article benchmarks yq 4.44.6, PyYAML 6.0.3 and js-yaml on a 2.39 MB sample, explains why the input extension decides the output format, and gives correct recipes for batch conversion, quoting, indentation and big-integer precision.

Intermediate~9 min readUpdated 2026-11-03