XML to YAML Converter
Convert to YAML, with the values YAML would misread quoted.
Everything runs in this tab. Nothing you paste is uploaded, logged or sent anywhere. Open your network panel and check.
Paste XML above and YAML appears beside it. The document is checked for well-formedness, mapped to a tree, then written out by an emitter whose main job is deciding which values have to be quoted. Nothing is uploaded: the scanner, the mapper and the emitter all run in this tab.
The usual reason to want this is that a configuration file, a Kubernetes manifest, a CI pipeline or an Ansible inventory needs data that currently lives in XML. The output goes into a file a machine reads literally, which is why the quoting matters more than the layout.
YAML looks like the friendly format and is the one most likely to silently change your data. An unquoted country code of NO becomes the boolean false in most of the ecosystem. A postcode of 01730 becomes 1730. A version of 1.10 becomes 1.1. This emitter quotes the values that would otherwise be misread, and this page says exactly which ones and why.
The mapping is the XML to JSON mapping
YAML 1.2 was designed as a superset of JSON, so there is no separate tree here. The XML is converted to the same structure the XML to JSON page produces and a different serialiser writes it out. Every mapping decision on that page applies unchanged: attributes become prefixed keys, text sharing an element with attributes or children goes under a text key, an element appearing twice becomes a sequence, and comments are dropped.
One thing gets worse. In JSON a consumer at least sees brackets; in YAML the difference between one item and two is an indented scalar versus a list of dashes, and nobody spots that in a diff. Use the "always an array" field for anything that is conceptually a list, so a one-item document and a fifty-item document produce the same shape.
<order id="00042">
<total currency="GBP">19.90</total>
<line sku="0071">Widget</line>
<line sku="0072">Gasket</line>
<country>NO</country>
</order>
order:
attr_id: '00042'
total:
attr_currency: GBP
text: '19.90'
line:
- attr_sku: '0071'
text: Widget
- attr_sku: '0072'
text: Gasket
country: 'NO'The Norway problem, and the exact list it covers
YAML 1.1 defines its boolean type by enumeration, and the enumeration is wider than anyone expects. The published type page lists, verbatim: y, Y, yes, Yes, YES, n, N, no, No, NO, true, True, TRUE, false, False, FALSE, on, On, ON, off, Off, OFF. Every one of those, unquoted, loads as a boolean.
The consequence has a name. A dataset of ISO country codes gets NO for Norway and the parser hands the application false. The same list swallows a Yes/No column exported from a spreadsheet and any switch spelled on or off that was meant to be text. YAML 1.2 narrowed the core schema to true and false only, but PyYAML, Ruby's Psych, Ansible and much Kubernetes tooling still resolve the 1.1 set, so assume all of it is live.
The emitter single-quotes any scalar matching that list exactly, including the single-letter forms, plus null, Null, NULL and the tilde. Note the case sensitivity: yES and nO are not in the 1.1 list and are not quoted, because no conforming parser reads them as booleans either.
What else gets quoted, and what slips through
The boolean set is the famous case, not the common one. Most values that break are numbers that were never numbers, because YAML infers a type from the spelling of a plain scalar exactly the way JSON does not. A scalar is single-quoted when it matches the boolean or null set, when it matches a JSON number grammar (covering 42, 19.90 and 1.10), when it has a leading zero followed by more digits, when it is empty, when it begins with a YAML indicator character such as a hyphen or a hash, or when it has whitespace at either end.
Multi-line text is not quoted. It becomes a literal block scalar introduced by a pipe with a strip indicator. Literal is chosen over folded deliberately: a folded block reflows single newlines into spaces, destroying embedded code and addresses. The strip indicator removes the trailing newline a block would otherwise add.
Some values still leave the emitter unquoted and can change type downstream. They are listed rather than glossed over, because no emitter using plain scalars has solved YAML type inference:
- Sexagesimal numbers. YAML 1.1 reads 22:22 as a base-60 integer, so a duration becomes 1342 in PyYAML. A 1.2 parser such as js-yaml returns the string, so this depends on which side reads the file.
- Hexadecimal spellings. 0x1F loads as 31 in both YAML 1.1 and the 1.2 core schema, so a hex colour code needs quoting.
- Dates. 2024-01-05 matches the YAML timestamp type, so js-yaml and PyYAML both hand you a date object rather than a string.
- Block scalars whose first line is indented further than the lines after it, which happens when a CDATA section preserves leading spaces. YAML's fix is an explicit indentation indicator after the pipe, which this emitter does not write.
Set the attribute prefix and the text key before you convert
This is the one piece of setup worth doing. The defaults were chosen for JSON, where they are safe, and YAML has a stricter grammar for keys than for values.
The default attribute prefix is @_ and the default text key is #text. In YAML, @ is a reserved indicator a plain scalar may not begin with, so a key of @_id makes the document fail to parse: js-yaml reports "bad indentation of a mapping entry" and PyYAML reports a character that cannot start any token. A leading # is worse because it does not fail. A line reading #text: 19.90 is a comment, so the file loads and the value is simply not there.
Both fields sit in the control row above the editor. Set the prefix to something plain such as attr_ and the text key to text, and every key in the output is an ordinary YAML name. Keys needing quotes for other reasons, such as soap:Body, are quoted automatically, because a colon is not legal in a bare key.
Doing this in code
Two steps: parse the XML safely, then serialise with a dumper you have told to quote properly. The XML half needs the usual entity flags, because the defaults in Java and .NET will resolve a DOCTYPE. The YAML half needs attention because dumpers differ on how aggressively they quote.
import { XMLParser } from 'fast-xml-parser';
import yaml from 'js-yaml';
const parser = new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: 'attr_', // not @_: YAML reserves a leading @
textNodeName: 'text', // not #text: a leading # is a comment
parseTagValue: false, // keep values as strings
parseAttributeValue: false,
processEntities: false, // do not expand DOCTYPE-declared entities
isArray: (name) => ['line', 'item', 'entry'].includes(name),
});
const out = yaml.dump(parser.parse(xmlSource), {
lineWidth: -1, // never fold long lines; folding rewrites your data
noRefs: true, // never emit anchors and aliases
quotingType: "'",
sortKeys: false,
});
// js-yaml's dumper is conservative: it quotes NO, 01730, 1.10, 22:22 and
// 0x1F on its own, and quotes keys that begin with @ or #. Add
// forceQuotes: true if you want every string quoted regardless.import xmltodict
import yaml
doc = xmltodict.parse(
xml_source,
disable_entities=True, # blocks the expat entity attacks
attr_prefix='attr_',
cdata_key='text',
force_list=('line', 'item', 'entry'),
)
print(yaml.safe_dump(
doc,
default_flow_style=False,
allow_unicode=True,
sort_keys=False,
width=10 ** 9, # effectively disable line folding
))
# PyYAML implements the YAML 1.1 resolver, so its dumper knows that NO,
# 01730 and 1.10 would load back as a bool, an int and a float, and quotes
# them. Use safe_dump, never dump: the full dumper emits Python-specific
# tags that only yaml.unsafe_load can read back.import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.dataformat.xml.XmlFactory;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
import javax.xml.stream.XMLInputFactory;
XMLInputFactory input = XMLInputFactory.newFactory();
input.setProperty(XMLInputFactory.SUPPORT_DTD, false);
input.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
JsonNode tree = new XmlMapper(new XmlFactory(input)).readTree(xmlSource);
YAMLMapper yaml = YAMLMapper.builder()
.disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER)
.disable(YAMLGenerator.Feature.MINIMIZE_QUOTES) // off is the safe state
.enable(YAMLGenerator.Feature.LITERAL_BLOCK_STYLE)
.build();
String out = yaml.writeValueAsString(tree);
// MINIMIZE_QUOTES is the setting to leave alone. It is off by default, and
// turning it on is how a value of NO ends up unquoted in a Jackson-generated
// file that a Python service then reads as false.using System.Xml;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using YamlDotNet.Core;
using YamlDotNet.Serialization;
var settings = new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Prohibit,
XmlResolver = null,
MaxCharactersFromEntities = 1024 * 1024,
};
using var reader = XmlReader.Create(new StringReader(xmlSource), settings);
var document = new XmlDocument { XmlResolver = null };
document.Load(reader);
string json = JsonConvert.SerializeXmlNode(document);
object? tree = JsonConvert.DeserializeObject<JObject>(json)?.ToObject<object>();
var serialiser = new SerializerBuilder()
.WithDefaultScalarStyle(ScalarStyle.SingleQuoted) // quote everything
.Build();
Console.Write(serialiser.Serialize(tree));
// WithDefaultScalarStyle is blunt: every scalar comes out quoted, including
// the ones that did not need it. That is the right trade for generated data.
// Drop it only if you are hand-checking the output.# yq v4 (Mike Farah) converts directly and quotes ambiguous scalars.
yq -p=xml -o=yaml '.' document.xml
# Match the key convention used on this page:
yq -p=xml -o=yaml \
--xml-attribute-prefix='attr_' \
--xml-content-name='text' \
'.' document.xml > out.yaml
# Then load it back with the parser that will actually consume it. This is
# the only check that proves nothing changed type on the way through:
python -c "import yaml; print(yaml.safe_load(open('out.yaml'))['order']['country'])"
# expect: NO not: FalseThe failure this page is about is silent. A YAML file with an unquoted NO in it parses cleanly, validates cleanly and deploys cleanly; the country is simply false from then on. The check that catches it is loading the generated file back with the same library the consumer uses and comparing a known-awkward value, not reading the diff.
Common questions
Does my XML get uploaded when I convert it to YAML?
No. The XML scanner, the tree mapper and the YAML emitter are all JavaScript in this tab, and there is no endpoint for them to post to. Open the Network tab in your developer tools, paste a document, and watch nothing happen.
Worth confirming rather than assuming, because XML converted to YAML is very often configuration. Connection strings, service accounts, API keys and internal hostnames all end up in the kind of document people bring to a converter.
What is the Norway problem?
YAML 1.1 defines its boolean type as a fixed list of spellings, and that list includes n, N, no, No and NO. So a field holding the ISO code for Norway, written without quotes, loads as false. The same list swallows y and Y, on and off, and any Yes/No column exported from a spreadsheet.
YAML 1.2 narrowed the core schema to true and false only, which has not fixed the ecosystem: PyYAML, Psych, Ansible and much Kubernetes tooling still resolve the 1.1 set, and you rarely control which parser reads your file. The emitter quotes every spelling in that list, so NO stays the string NO.
Why are some values wrapped in quotes and others are not?
Because the quotes are load-bearing. A plain YAML scalar has its type inferred from how it is spelled, so 01730 is a number, 1.10 is a float, NO is a boolean and a leading hyphen starts a list item. Quoting is how you say it is text.
The emitter quotes exactly the values that would otherwise change type or meaning and leaves everything else plain, because quoting every scalar makes a file harder to read and diff for no benefit. For uniform quoting, most YAML libraries have a force-quotes option; the samples above show it for js-yaml and YamlDotNet.
What happens to multi-line text content?
It becomes a literal block scalar, introduced by a pipe with a strip indicator, with the lines indented beneath it. Literal was chosen over folded on purpose: a folded block reflows single newlines into spaces, quietly destroying embedded code and addresses.
One case to watch. If the first line of the text is indented further than the lines after it, which happens when a CDATA section preserves leading spaces, the block is ambiguous and a parser will reject it.
Do repeated elements become YAML lists?
Yes. An element appearing more than once under the same parent becomes a sequence written as a list of dashes; one appearing once becomes a plain nested mapping or scalar. That is the same singleton ambiguity the XML to JSON page describes, and it is more dangerous here because YAML hides it: the difference between one item and two is a dash and two spaces of indentation.
Use the "always an array" field above the editor. Name the elements that are conceptually lists and they are emitted as sequences whether the document holds one of them or forty.
Are XML comments and namespaces preserved?
Comments are not. They are dropped when the document is mapped to a tree, before the emitter sees anything. YAML comments are not part of the data model, so one written into the output would vanish the first time anyone loaded and re-saved the file.
Namespace prefixes are kept verbatim, so soap:Body becomes a key spelled soap:Body, quoted automatically because a colon is not legal in a bare YAML key. Ticking "strip namespace prefixes" gives plain Body instead, at the risk of merging two namespaces onto one key.