The same two records shown as XML on the left and as TOML on the right, field by field.
The same two records as XML and as TOML. The values are the ones that usually break: NO is a boolean in YAML unless it is quoted, and 1.10 and 2.0 change value the moment a converter types them as numbers instead of keeping them as strings.

Why convert XML to TOML at all

This is a migration direction. The configs of the 2000s were XML, Java properties trees, .NET app.config, build tool descriptors, and the services around them are being rewritten in ecosystems whose configuration language is TOML: Rust throughout, Python packaging, modern CLIs. Somebody sits between a 400-line legacy XML config and an application that wants config.toml, and retyping 400 lines by hand is how values get lost.

It works as well as it does because old-style config XML barely uses XML's hard parts: it is a tree of named settings with attributes, no mixed content, no significant order. That subset maps onto TOML tables almost one to one, and the leftover conventions (attributes, text-with-attributes) are the subject of this page.

How to use this converter

  1. Paste or drop your XML. Well-formedness is checked first; errors show inline with line and column.
  2. Review the TOML on the right, particularly quoted keys like "@_host", which mark where XML-specific conventions entered.
  3. Copy or download the .toml and adapt key names to what the consuming application expects.

--keep-attributes

On by default: attributes become @_ prefixed keys. Off, they are dropped entirely, useful when the attributes were ids and metadata the new config does not need.

--parse-numbers

On by default: numeric text becomes real TOML integers and floats, so <port>5432</port> arrives as port = 5432, not "5432". Off, every value stays a string, the safe mode for version numbers and identifiers with leading zeros.

--sort-keys

Alphabetical key order per table, for diffing. Off by default.

How XML constructs map to TOML

XMLTOML
<title>Deploy</title>title = "Deploy"
<database> with children[config.database] table
repeated <server> siblings[[config.server]] sections
host="db.internal" attribute"@_host" = "db.internal"
<total currency="EUR">9.5</total>table with "#text" and "@_currency"
<empty/>empty = ""
comments, PIs, DOCTYPEnot carried over

The root element becomes the outermost table, so everything in the file sits under one header named after it. Deeper nesting produces dotted headers ([config.database.pool]), which stay readable to about three levels and get unwieldy past that, a property of TOML rather than of the conversion.

A table comparing what XML and TOML can represent: comments, typed values, explicit null, nested structures and a top-level list.
Both formats can represent the same things here, so this direction loses nothing structural. That is not true of the way back for every pair, which is why the table is per direction and not per format.

Attributes and elements with both text and attributes

XML gives every element two value channels, attributes and content, and TOML has one. The @_ prefix keeps them apart after conversion: <database host="db.internal"> yields a "@_host" key inside the [config.database] table, quoted because @ is not a bare-key character. The prefix matches our other XML converters, so the same document converted to JSON or TOML produces parallel key names.

When an element carries text and attributes at once, the text needs a name of its own, and that name is "#text": <total currency="EUR">9.5</total> becomes a small table holding "#text" = 9.5 and "@_currency" = "EUR". It looks odd on first sight and is the only faithful encoding; a consumer that wants total = 9.5 plain should get a source where the currency is its own element, or a post-edit.

Numbers: parse or preserve

Everything in XML is text, so 5432 in an element and the string "5432" are the same bytes; whether they should become a TOML integer is a decision, not a fact. With --parse-numbers on, numeric-looking text converts to real numbers, which is what configs want, ports, timeouts, pool sizes turn into values the application can use unquoted. Booleans spelled true/false come along the same way.

The flag exists because parsing has victims: a version 1.10 becomes the float 1.1, an article number 007 loses its zeros, a 20-digit ID exceeds 64-bit integer range. When the file contains identifiers dressed as numbers, switch the flag off and everything stays a faithful string; TOML quotes make the type explicit either way.

What does not fit, and what to do instead

  • Mixed content. Text interleaved with elements collapses into a "#text" key and loses its ordering. Document-shaped XML should not come here at all.
  • Comments. Dropped, as in every converter that goes through parsed data. TOML comments use #; re-add the ones that carried decisions.
  • Namespaces. Prefixes survive as literal key text ("soap:Body"), the namespace machinery does not. Fine for value extraction, wrong for namespace-aware processing.
  • Single-vs-repeated ambiguity. One <server> converts as a table, two as an array of tables, because XML cannot say "list of one". If the consumer needs a stable shape, normalize after converting.
  • Order between different elements. TOML tables are unordered by data model; XML sibling order across different names is not preserved as a feature. Config consumers do not care; if yours does, it wanted XML.

For the documents these limits exclude, the neighboring conversions are the better fit: XML to YAML for a readable tree view of arbitrary XML, or XML to CSV when the file is really a record list bound for a spreadsheet.

Where the two formats disagree

How do I convert XML to TOML in Python?

Chain two libraries: xmltodict parses the XML into a dict, tomli_w writes the dict as TOML. tomli_w.dump(xmltodict.parse(open("config.xml", "rb").read()), open("out.toml", "wb")) is the whole program. Two things bite: xmltodict returns everything as strings, so numbers arrive quoted unless you post-process, and elements that appear once parse as a dict while repeated ones parse as a list, so a one-entry config converts to a different shape than a two-entry one. Both behaviors are XML's fault, not the libraries'.

Is there a standard mapping between XML and TOML?

No. Neither specification mentions the other, and the two disagree on fundamentals: XML separates attributes from child elements and allows text mixed between children, TOML has only keys and values. Every converter therefore invents conventions, most commonly a prefix for attributes (this tool uses @_) and a reserved key for text content (#text). The practical consequence is that converted files from different tools differ in exactly those keys; pick one tool for a migration and keep using it.

Why would anyone convert XML config files to TOML?

Because the application around the config changed generations. XML was the default config format of the 2000s (Java, .NET, Maven, log4j), and teams porting such services to Rust or modern Python land in ecosystems whose tools read TOML. The conversion moves the values across the boundary; the shapes carry over well because old XML configs are trees of settings with little mixed content, which is exactly what TOML tables express. What does not carry over are comments and processing instructions, so the documentation lives in the migration commit instead.

How do repeated XML elements convert to TOML?

They collapse into one key holding an array: three <server> siblings become a single server entry written as three [[…server]] sections. This is the standard resolution of a mismatch, XML expresses lists by repeating names, TOML by holding an array under one name. The corner case is a single occurrence: one <server> is indistinguishable from a scalar entry, so it converts as a table, not a one-element array of tables. Consumers reading the TOML should tolerate both shapes, or the source should guarantee at least two entries.

Can TOML represent mixed content like <p>Hello <b>world</b></p>?

Not meaningfully. Mixed content, text interleaved with child elements, is XML's document side, and TOML's data model (nested tables of typed values) has no slot for "text between two keys". Parsers reduce it to a #text key holding the concatenated fragments next to the child keys, which loses the interleaving order. If your XML is document-like (DocBook, XHTML, anything with markup in sentences), TOML is the wrong target; this conversion is for the config and data-tree side of XML.

Does TOML have namespaces like XML?

No, and converted namespaced XML shows it: a soap:Envelope element arrives as a key literally named "soap:Envelope", quoted because of the colon, with the xmlns declarations as ordinary @_ attribute keys. TOML sees the prefix as part of the name, nothing more. That is fine for reading values out of a SOAP response by hand, and useless for namespace-aware processing, prefix and URI stay disconnected. Strip namespaces before converting when the consumer only cares about local names.