Skip to content
datahacks.dev
XML

Common XML Formats You Will Meet in Real Work Reference

A reference to the XML formats you meet in practice: how to recognize each by root and namespace, what to extract, and the trap each one carries.

19 min read
Summarize in AI
Common XML Formats You Will Meet in Real Work Reference

XML is rarely met in the abstract. It arrives as a particular format: a sitemap, a feed, a build file, a map track, an SVG, a response from a web service. Each format has a vocabulary, a namespace, a handful of conventions and one or two traps that bite everyone once. Knowing which format you hold is half the work of reading it, and the other half is knowing what the parser cannot tell you.

This is a reference to the formats I have met most, with a method for recognizing a file you have never seen, the namespace of each, what to extract, and the thing to watch. Most entries link to a longer article. It is meant to be skimmed and returned to.

Recognizing a format from its first bytes

A file's extension is a hint and its first start tag is the evidence. The root element name, its namespace and its version attribute identify nearly every format in this guide, and reading them costs one parse event. Here is the sniffer I use, with the output on twenty sample files.

sniff.pyPython
import glob
from lxml import etree

def sniff(path):
    with open(path, "rb") as f:
        head = f.read(4)
    if head[:2] == b"PK":
        return ("zip", "(Office, ODF, vsdx, kmz or epub: open the zip)", "")
    try:
        for ev, el in etree.iterparse(path, events=("start",), load_dtd=False,
                                      resolve_entities=False, no_network=True):
            q = etree.QName(el)
            return (q.localname, q.namespace or "-", el.get("version") or "")
    except etree.XMLSyntaxError as e:
        return ("NOT XML", str(e.msg)[:40], "")
Text
app.csproj     Project        -
app.plist      plist          -                                                    1.0
ecb.xml        Envelope       http://www.gesmes.org/xml/2002-08-01
feeds.opml     opml           -                                                    2.0
icon.svg       svg            http://www.w3.org/2000/svg
library.xml    library        -
log4j2.xml     Configuration  -
manifest.xml   manifest       -
messages.xlf   xliff          urn:oasis:names:tc:xliff:document:1.2                1.2
places.kml     kml            http://www.opengis.net/kml/2.2
plan.xml       Project        http://schemas.microsoft.com/project
pom.xml        project        http://maven.apache.org/POM/4.0.0
report.docx    zip            (Office, ODF, vsdx, kmz or epub: open the zip)
route.gpx      gpx            http://www.topografix.com/GPX/1/1                    1.1
rss.xml        rss            -                                                    2.0
sitemap.xml    urlset         http://www.sitemaps.org/schemas/sitemap/0.9
soap_fault.xml Envelope       http://schemas.xmlsoap.org/soap/envelope/
soap_ok.xml    Envelope       http://schemas.xmlsoap.org/soap/envelope/
strings.xml    resources      -
web.xml        web-app        https://jakarta.ee/xml/ns/jakartaee                  6.0

The sniffer passes load_dtd=False, resolve_entities=False and no_network=True, because identifying an unknown file is exactly the moment you should not let it fetch anything. Read the output as a lookup. A root of urlset in the sitemap namespace is a sitemap. An Envelope is a SOAP message in one namespace and the ECB's rate file in another, so the name alone fails. Project appears twice: with the Microsoft namespace it is a Project plan, and with no namespace and a Sdk attribute it is a .NET project file. And pom.xml has a lowercase project, in the Maven namespace. The first lesson of the table is that the namespace disambiguates what the name cannot.

The second lesson is the zip row. A docx, an xlsx, a Visio drawing, a KMZ map, an ODF document and an EPUB are zip archives holding XML parts, and they all start with the two bytes PK. A sniffer that finds those opens the archive and sniffs its parts.

The formats

Data feeds and discovery files

Sitemap (urlset or sitemapindex, namespace http://www.sitemaps.org/schemas/sitemap/0.9). A list of URLs with optional dates. Extract loc and lastmod. Trap: the default namespace, so an unprefixed url path finds nothing; also extension children (images, video, alternate languages) in other namespaces that a naive reader skips or confuses with loc. See reading a sitemap XML file and parsing an XML sitemap into a table.

RSS 2.0 (rss, no namespace on core elements). Items under channel, with RFC 822 dates and an optional guid. Trap: description may be text or HTML, and durations in podcast feeds appear in two formats.

Atom 1.0 (feed, http://www.w3.org/2005/Atom). Entries with a required id and updated, ISO 8601 dates, links as attributes. Trap: it is namespaced where RSS is not, so one reader needs both code paths. See RSS and Atom feeds explained for developers and analyzing podcast and blog feeds from XML.

OPML (opml, no namespace). A list of feed subscriptions as nested outline elements with xmlUrl attributes. Read: //outline[@xmlUrl]. In my sample that gave the two feeds. Trap: outlines nest for folders, and a reader must descend; also all the data is in attributes, none in text.

Product feeds (RSS with a g: namespace for shopping platforms). Covered in loading a product feed in XML into PostgreSQL.

Messages and web services

SOAP 1.1 (Envelope, http://schemas.xmlsoap.org/soap/envelope/). A Header and a Body that holds the response element or a Fault. Trap: the payload is in the service's own namespace, a second one, and the fault's children are unnamespaced. See SOAP responses and pulling the payload out.

XLIFF 1.2 (xliff, urn:oasis:names:tc:xliff:document:1.2). Translation units with a source and a target. Extract: id, source, target. In the sample, greet had a French target and bye had none, so a missing translation is a missing element and not an empty one. Trap: inline markup inside source and target (placeholders, bold) makes them mixed content (dealing with mixed content in XML text).

Documents and graphics

SVG (svg, http://www.w3.org/2000/svg). Shapes, text and links. Extract values from plain shapes, labels from text. Trap: the namespace, a second xlink namespace for older link syntax, and transforms that change what coordinates mean. See reading SVG as XML data.

Office Open XML (a zip; word/document.xml, xl/worksheets/sheet1.xml). Text is in w:t inside runs inside paragraphs; cells hold shared string indexes. Trap: runs split words arbitrarily, number cells are dates only by their style. See reading Office Open XML from docx and xlsx files.

Visio (.vsdx, a zip). Shapes with text and connectors in visio/pages. I did not generate one; the zip method is the same.

Location and sensor data

GPX (gpx, http://www.topografix.com/GPX/1/1). Tracks, routes and waypoints with lat and lon attributes. Trap: elevation noise, pauses, and extensions in other namespaces.

KML (kml, http://www.opengis.net/kml/2.2). Placemarks and shapes with coordinates as text. Trap: longitude comes first, the reverse of GPX's attribute order; descriptions are HTML in CDATA. See reading KML and GPX location files.

Build and configuration files

Maven POM (project, http://maven.apache.org/POM/4.0.0). Dependencies with versions that may be ${property} placeholders. Trap: parents, dependency management and profiles, which the single file does not show.

Android manifest (manifest, no namespace, but android: attributes in http://schemas.android.com/apk/res/android). The inverse of the usual trap: the elements are plain and the attributes are namespaced. Android resources (resources) hold strings, plurals and arrays, with backslash escapes the parser leaves in.

log4j2 (Configuration, usually none). Loggers with levels; the root is a separate Root element. web.xml (web-app, a Jakarta or Java EE namespace that changed over versions). .NET project (Project with an Sdk attribute, no namespace in the SDK style). See analyzing Android and Maven XML files and reading XML config files from Maven and Android.

Apple property list (plist, no namespace, with a DOCTYPE). Alternating key and value elements: string, integer, true, array, dict. The standard library reads it:

Python
import plistlib
print(plistlib.load(open("app.plist", "rb")))
Text
{'CFBundleName': 'Demo', 'LSMinimumSystemVersion': '13.0', 'Enabled': True, 'Count': 3, 'Tags': ['a', 'b']}

plistlib handled the typed values for me: the boolean is a real True, the integer an int. Trap: the structure pairs a key with the next sibling, so a generic XML reader that treats dict as a mapping gets it wrong; use plistlib for XML plists, and note that binary plists exist and plistlib reads those too.

Business and government data

Project plans (Project, http://schemas.microsoft.com/project). Tasks with ISO durations in working hours. See reading Microsoft Project and Visio XML exports.

Reference data and statistics. The ECB reference rates file has two namespaces and stores rates as attributes. Others use XBRL or SDMX. See reading financial and government XML datasets.

Schemas and transformations

XSD (schema, http://www.w3.org/2001/XMLSchema), XSLT (stylesheet, http://www.w3.org/1999/XSL/Transform), Schematron, RELAX NG. These are XML describing XML. Validation is in validating XML against an XSD schema, and transformation in XSLT and XQuery for real transformations.

A registry of readers

Once you can name the format, you can dispatch to a reader. A small registry keyed on the pair (root name, namespace) turns the sniffer into a tool that opens any file it knows and says so plainly when it does not. Each reader is a few lines and returns a one-line summary, which is what a first look should produce:

registry.pyPython
READERS = {}
def reader(local, ns):
    def deco(fn):
        READERS[(local, ns)] = fn
        return fn
    return deco

@reader("urlset", "http://www.sitemaps.org/schemas/sitemap/0.9")
def sitemap(r): return f"{len(r)} urls, first {r[0][0].text}"

@reader("rss", None)
def rss(r): return f"{len(r.findall('channel/item'))} items in '{r.findtext('channel/title')}'"

@reader("gpx", "http://www.topografix.com/GPX/1/1")
def gpx(r): return f"{len(r.findall('.//{http://www.topografix.com/GPX/1/1}trkpt'))} track points"

def read(path):
    root = etree.parse(path, etree.XMLParser(resolve_entities=False, no_network=True, load_dtd=False)).getroot()
    fn = READERS.get(q(root))
    return fn(root) if fn else f"no reader for {q(root)}"

Run over the sample files, with a reader registered for eight formats:

Text
sitemap.xml    3 urls, first https://example.test/
rss.xml        3 items in 'Dev Talk'
route.gpx      3 track points
places.kml     2 placemarks
icon.svg       viewBox 0 0 100 60, 3 rects
feeds.opml     2 feeds
soap_ok.xml    payload GetPriceResponse
messages.xlf   2 units
plan.xml       no reader for ('Project', 'http://schemas.microsoft.com/project')
strings.xml    no reader for ('resources', None)

The last two lines are the design working as intended. A file with no registered reader is reported with the exact key it would need, which is the root and namespace you would paste into a new @reader line. Dispatching on the pair also solves the Project ambiguity without a special case, since the plan and the .NET file have different keys. The registry is the right size for an inbox of mixed files, an ingestion service that must refuse what it does not understand, or a command-line tool that gives a first impression of a download. Notice what it does not do: it never guesses from the extension, and it never falls through to a generic reader for unknown formats, because a generic reader returns plausible nonsense.

Add a size check and a depth check before the reader runs, and a time limit if the file comes from outside, as in limiting XML size and depth for uploads.

Encodings, byte order marks and the first line

The sniffer reads bytes, and the first bytes carry information about the encoding that a text reader loses. I tested six small documents containing the letter e with an acute accent:

Text
utf8       first bytes b'<?xm'                      -> 'é'
utf8 bom   first bytes b'\xef\xbb\xbf<'             -> 'é'
utf16 bom  first bytes b'\xff\xfe<\x00'             -> 'é'
latin1     first bytes b'<?xm'                      -> 'é'
no decl    first bytes b'<a>\xc3'                   -> 'é'
lies       first bytes b'<?xm'                      -> ERROR Invalid bytes in character encoding, line 1, column 42

Five of the six parse to the right character. The parser handles a UTF-8 byte order mark (ef bb bf), UTF-16 with its mark (ff fe), an ISO-8859-1 file whose declaration says so, and a file with no declaration, which defaults to UTF-8. The sixth is the file whose declaration says UTF-8 and whose bytes are Latin-1: the byte e9 is not valid UTF-8, and the parser stops with Invalid bytes in character encoding and a column. That one is the commonest real-world failure, and its repair is a conversion with the right source encoding (encoding mismatch errors in XML documents).

One more first-line rule bit me: whitespace before the XML declaration is an error, not a harmless indent.

Text
space before declaration -> XML declaration allowed only at the start of the document, line 1, column 8

The declaration must be the very first thing in the file, with no spaces, no blank line and no byte order mark before it other than the mark itself. Files produced by templates often start with a newline, and the error message does not make the cause obvious. Strip the leading whitespace and retry.

Formats change, and their names with them

Formats are versioned, and the version usually shows in the namespace or the version attribute, which is why the sniffer prints both. Some examples from this guide: GPX is at 1.1 in the sample, and 1.0 files use a different namespace. SOAP 1.2 uses a different envelope namespace from 1.1. Java EE descriptors moved from http://java.sun.com/xml/ns/javaee to http://xmlns.jcp.org/xml/ns/javaee and then to the Jakarta namespace. KML has 2.2 and 2.3 in different namespaces. A reader that hardcodes one URI works until it meets the next version of the same format.

There are three ways to cope. List every URI you support and treat an unknown one as an error that names itself, as the registry does. Match on the local name only (local-name()) when the versions are compatible for your purpose, accepting the risk of confusing two vocabularies. Or read the version first and branch. Whichever you choose, put the supported versions in a test, with a sample of each, so that dropping or adding one is a deliberate change.

The same applies to your own formats. If you design one, give it a namespace with a version in it from the first release, and plan how a reader will tell versions apart. The cost of doing it on day one is a line; the cost of adding it later is every consumer.

What these formats teach about designing your own

A reader of many formats develops opinions. These are mine, from the traps above:

  • Say the units. A bare number is a trap. The rate file says what it is a rate of only in the documentation; a price element without a currency, or a duration without a calendar, is half a fact. Put the unit in an attribute or the name.

  • Choose one place for each fact. Attributes for identifiers and metadata, elements for content, and do not mix them for the same field. Android puts the name of everything in an attribute, GPX puts positions in attributes and elevation in elements, and every reader must remember which.

  • Use stable identifiers. Atom requires an id and RSS makes its guid optional, and the difference shows up as duplicate items. The UID against ID split in Project is the same lesson: the visible row number is not a key.

  • Order means something or it does not, and say which. KML coordinates are ordered; sibling tags in a config are not. A schema that pins the order of unordered things breaks senders for no reason.

  • Leave room. An extension point (xs:any, extensions elements as in GPX) lets a format grow without breaking old readers.

  • Do not hide HTML in text. RSS description and KML description are both "text or HTML, guess which". A type attribute, as in Atom, ends the guessing.

The namespace table

The namespaces in this guide, in one place, since they are the part nobody remembers:

FormatNamespace URI
Sitemaphttp://www.sitemaps.org/schemas/sitemap/0.9
Atomhttp://www.w3.org/2005/Atom
SVGhttp://www.w3.org/2000/svg
XLinkhttp://www.w3.org/1999/xlink
GPX 1.1http://www.topografix.com/GPX/1/1
KML 2.2http://www.opengis.net/kml/2.2
SOAP 1.1http://schemas.xmlsoap.org/soap/envelope/
XLIFF 1.2urn:oasis:names:tc:xliff:document:1.2
Microsoft Projecthttp://schemas.microsoft.com/project
Maven POMhttp://maven.apache.org/POM/4.0.0
Android attributeshttp://schemas.android.com/apk/res/android
Jakarta EE descriptorshttps://jakarta.ee/xml/ns/jakartaee
WordprocessingMLhttp://schemas.openxmlformats.org/wordprocessingml/2006/main
SpreadsheetMLhttp://schemas.openxmlformats.org/spreadsheetml/2006/main
XML Schemahttp://www.w3.org/2001/XMLSchema
XSLT 1.0http://www.w3.org/1999/XSL/Transform

A namespace URI is an identifier and not an address. It need not resolve, and a typo in one (http against https, a missing trailing slash) makes a different name. Copy the string from the file, not from memory. Where a format has had several versions, the namespace usually changes with the version (SOAP 1.2, Java EE against Jakarta EE, GPX 1.0), and a reader that supports the fleet needs a list.

What the parser cannot tell you

Every format has a fact that lives outside the file. These were the ones I found:

  • KML order. Longitude first, then latitude, then altitude.

  • Project durations. Working hours under a calendar, not elapsed time.

  • RSS description. Text or escaped HTML, undeclared.

  • Android strings. A backslash before an apostrophe stays in the parsed text.

  • Spreadsheet dates. Numbers whose style makes them dates.

  • SVG coordinates. Meaning depends on the viewBox and any transform.

  • Rates. A direction: the ECB's USD rate is dollars per euro.

  • Sitemap dates. A claim by the publisher.

  • XLIFF targets. A missing element means untranslated, an empty one means translated to nothing.

  • plist pairs. A key belongs to the following sibling.

When reading a format you do not know, search for the equivalent list: the section of the specification that says what each field means, not what shape it has.

Reading any of them, in five steps

  1. Sniff. Root, namespace, version. Zip or not.

  2. Check well-formedness. xmllint --noout. If it fails, repair before anything else (fixing XML parse errors fast).

  3. Bind the namespaces in a dictionary, with prefixes you choose.

  4. Profile the paths (counting elements and attributes in an unknown XML file) to find the repeating record and the optional fields.

  5. Read the meaning from the specification for the traps above, and reconcile a count or a sum against something you know.

Parse unknown files with a conservative parser: no DTD loading, no entity resolution, no network. The attacks are described in XML security problems to close before production, and the same flags are on the sniffer above.

The first ten minutes with each format

What I do first with each format, and the one check that tells me I have read it correctly. None of these is the whole job; they are the opening moves.

Sitemap. Count url elements with the namespace bound, and count distinct lastmod values. If the second is 1 and the first is 5,000, the dates are build times. Check that the root is exactly urlset or sitemapindex in the 0.9 namespace.

RSS and Atom. Parse the item dates with the right parser for the format, sort, and print the gaps between publication times. The gaps expose a dead feed or a bad date in one look. Check that every item has an identifier, and decide what to key on if not.

SVG. Print the viewBox, the number of shapes by type and every text label. Those three tell you whether the file is a chart, an icon set or a diagram, and whether a bitmap is hiding inside. Do not trust a number until you have read the transform on its group.

Docx. Print the paragraphs with their style names, so headings show up as Heading1, and join the runs before searching for a phrase. The check is that the text you extracted contains a sentence you can see in the document when you open it.

Xlsx. Print the cell address, type and value for the first rows, with the shared strings resolved. Look for numbers that should be dates and for strings that should be numbers. Compare the count of non-empty cells with what the application reports.

GPX and KML. Print the bounding box, the first and last point and the point count. If the box is in the wrong hemisphere, the order is swapped. For a track, print the largest gap in time and the largest jump in distance.

SOAP. Print the local name of the Body's first child. If it is Fault, print the fault string and stop. If it is a response, then read the fields, with the service namespace bound.

Project. Print each task's UID, name, outline level, duration in hours and predecessor UIDs. If the hours are not what the plan shows on screen, the calendar is not eight hours a day, and any total you compute needs it.

plist. Read it with plistlib, not with a generic XML mapping, and check the types: the sample returned a boolean, an integer and a list, which a generic reader would give back as text.

XLIFF. Count the units with a target and without. The ratio is the translation progress. Check that the source language and target language attributes are what you expect.

Each recipe is deliberately short. A reading that cannot be done in a few lines is usually a sign that the question needs a tool built for the format. And each ends in a check against something outside the parse, because a parser that returns a confident value for the wrong element is the characteristic failure of working with these files.

Safe handling of files from outside

The formats in this guide arrive from many places: downloads, attachments, partner uploads, forms. The same defaults apply to all of them.

No external resources. XML can name a DTD or an entity that lives at a URL or a file path. A parser that fetches them lets the sender make your machine read local files or request internal addresses. Turn that off (no_network=True, resolve_entities=False, load_dtd=False in lxml, the equivalents in other libraries), and prefer a library that is safe by default (safe XML parsing in Python with defusedxml).

Limits. An XML document can expand a few hundred bytes into gigabytes through nested entities (the "billion laughs" pattern), or nest tens of thousands of levels deep. A size cap before parsing and a depth cap while parsing keep a small file from taking down a large machine (stopping the billion laughs attack in your parser).

Zip containers. An Office file, a KMZ or a Visio file is a zip, and a zip can claim to hold small files that expand to enormous ones. Check each part's declared size before reading and cap the total, and never extract to disk using the names inside the archive without checking that they stay inside the target folder.

Content, not just structure. SVG can carry scripts, KML descriptions can carry HTML, and RSS descriptions can carry anything. Parsing the XML safely does not make its text safe to display. Sanitize before rendering, with an allow list.

Honesty about what you read. If a file is a format you do not recognize, say so and stop. Reporting "no reader for this root and namespace" is better than an extraction that matched the wrong element.

The articles behind this reference: reading a sitemap XML file, RSS and Atom feeds explained for developers, reading SVG as XML data, reading Office Open XML from docx and xlsx files, reading KML and GPX location files, reading XML config files from Maven and Android, SOAP responses and pulling the payload out and reading Microsoft Project and Visio XML exports. For the syntax rules underneath, start with the rules XML follows and how XML namespaces work.

The primary sources for the formats named here: the XML 1.0 specification, the Namespaces in XML recommendation, the sitemaps.org protocol, the RSS 2.0 specification, RFC 4287 (Atom), the SVG 2 specification and ECMA-376 for Office Open XML. Check a path against a sample in the XPath tester.

When a file looks like XML but is another format

**It starts with < and is HTML.** HTML5 is not XML: unclosed tags, unquoted attributes and named entities are normal. A strict XML parser rejects most real pages. Use an HTML parser, or XHTML if the page really is XHTML.

It is XML wrapped in something else. JSON with an XML string inside a field, a CSV cell holding a fragment, an email attachment encoded in base64. Unwrap first, then sniff.

It is a binary format with an XML look. Binary plists, old Office .doc and .xls files and some proprietary dumps start with different bytes. The sniffer's NOT XML branch catches them; do not try to repair them as XML.

It is a fragment. Many systems send an element with no root or several top-level elements. Wrap it in a root before parsing, and say so in your loader (fixing XML parse errors fast).

The extension lies. A .xml file that is a SOAP message, a .txt that is a sitemap. Trust the first tag over the name.

Also inTooling
Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.