XML for API testers
Elements, attributes and text, the rules that make a document XML, and escaping.
XML is a way of writing structured data as text, using tags in angle brackets. Before JSON took over web APIs, it was the standard format for data exchanged between systems, and it's still everywhere: SOAP web services, banking and payment systems, government and healthcare standards, RSS feeds, configuration files, and plenty of REST APIs. If you test APIs for long enough, you'll meet it. This lesson covers what you need to read an XML response and know when one is broken.
Running the examples
The tutorial's tests are in Java, using only what comes with the JDK plus JUnit to run them. You need Java 21 or later and Maven. Download the project, unzip it and run:
$ java -version
openjdk version "21.0.12.1" 2026-08-18 LTS
$ cd soap-xml
$ mvn test
...
[INFO] Tests run: 52, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESSEvery test calls the live API at api.testingapis.com, and none of them change anything there. The lessons also show each request with curl, so you can follow along without Java. If you've never set up a Maven project, the first lesson of the REST Assured tutorial walks through it.
The parts of an XML document
Here is a real XML response from this API:
$ curl -i https://api.testingapis.com/api/formats/xml
HTTP/1.1 200 OK
content-type: application/xml; charset=utf-8
<?xml version="1.0" encoding="UTF-8"?>
<users>
<user id="1">
<name>Alice Johnson</name>
<email>alice@example.com</email>
<roles><role>admin</role><role>editor</role></roles>
<active>true</active>
</user>
<user id="2">
<name>Bob Smith</name>
<email>bob@example.com</email>
<roles><role>viewer</role></roles>
<active>false</active>
</user>
</users>And the same kind of document with each part labeled:
<?xml version="1.0" encoding="UTF-8"?> ← the declaration: optional, always first
<users> ← the root element: exactly one per document
<user id="1"> ← an element with an attribute, id, whose value is "1"
<name>Alice Johnson</name> ← an element whose content is text
<roles> ← an element whose content is other elements
<role>admin</role>
<role>editor</role> ← a list is the same element repeated
</roles>
<manager/> ← an empty element: the same as <manager></manager>
</user>
</users>- An element is a start tag, its content and an end tag:
<name>Alice Johnson</name>. Elements nest inside each other to form a tree, with one root element at the top holding everything else. - An attribute is a name and a quoted value inside a start tag:
id="1". An element can have several, but each name only once. - Text is the content between tags. Here the text of
nameisAlice Johnson. - The declarationon the first line says which XML version and character encoding the document uses. It's optional, but when present it must come first, before even a blank line.
Whether a value goes in an attribute or an element is the designer's choice, and APIs differ. A common habit is attributes for identifiers and metadata, elements for data, but both <user id="1"> and <user><id>1</id></user> are fine XML. A test has to read whichever the API chose.
XML compared with JSON
If you know JSON, most of XML maps across. A few things don't:
JSON XML
{ <user id="1">
"id": 1, <name>Alice Johnson</name>
"name": "Alice Johnson", <roles>
"roles": ["admin", "editor"], <role>admin</role>
"active": true, <role>editor</role>
"manager": null </roles>
} <active>true</active>
<manager xsi:nil="true"/> (one convention among several)
</user>| JSON | XML | |
|---|---|---|
| Types | Strings, numbers, true and false, null | Everything is text. 1 and true are characters until code decides what they mean |
| Lists | Arrays: ["admin", "editor"] | No list type. A list is the same element repeated, often inside a wrapper element |
| Empty or missing | null, or the key left out | No null. An empty element, a missing element, or a convention such as xsi:nil |
| Extra information | Only keys and values | Attributes, comments, namespaces (lesson 2) and more |
| Size | Smaller | Larger: every element name is written twice |
The first row matters most for testing. In JSON, "active": true is a boolean. In XML, <active>true</active>is four characters, and a test that compares it with a boolean compares two different things. Lists are the second trap: a list with one item, like Bob's single role, looks exactly like a single value, so code has to know from the documentation, or a schema, that it may repeat.
Well-formed: the rules every document must follow
XML is strict. A document that breaks any of these rules is not well-formed, and a correct parser refuses to read it at all. There's no partial result, unlike HTML, which browsers try to make sense of whatever you write.
| Rule | Broken | Fixed |
|---|---|---|
| Exactly one root element | <a/><b/> | <list><a/><b/></list> |
| Every start tag has an end tag | <name>Ana | <name>Ana</name> |
| Tags close in the reverse order they opened | <b><i>text</b></i> | <b><i>text</i></b> |
| Names are case-sensitive | <Name>Ana</name> | <Name>Ana</Name> |
| Attribute values are quoted | <user id=1> | <user id="1"> |
| & and < are escaped in text | <note>Tom & Jerry</note> | <note>Tom & Jerry</note> |
Valid is a stronger word. A valid document is well-formed and also follows a schema that says which elements may appear where. Lesson 7 checks responses against a schema; for now, well-formed is the minimum. The test below sends a broken document to the parser:
@Test
@DisplayName("a document that isn't well-formed is not XML at all")
void wellFormed() {
IllegalArgumentException error = assertThrows(IllegalArgumentException.class,
() -> Xml.parse("<user><name>Ana</user>"));
assertTrue(error.getMessage().contains("must be terminated by the matching end-tag"), error.getMessage());
}The parser's message, "must be terminated by the matching end-tag", is Java's way of saying that <name> was never closed before </user> arrived.
Escaping and CDATA
Five characters have a special meaning in XML. Inside text and attribute values, they're written as entities, short codes between & and ;:
| Character | Written as | Must be escaped |
|---|---|---|
| & | & | Always |
| < | < | Always |
| > | > | Recommended |
| " | " | Inside attribute values in double quotes |
| ' | ' | Inside attribute values in single quotes |
For a block of text with many special characters, such as a code sample, a CDATA section avoids escaping everything: <![CDATA[Tom & Jerry <3]]>. Everything between <![CDATA[ and ]]> is plain text. Either way, the text a parser hands you is the same:
@Test
@DisplayName("special characters must be escaped, or wrapped in CDATA")
void escaping() {
assertThrows(IllegalArgumentException.class, () -> Xml.parse("<note>Tom & Jerry</note>"));
Document escaped = Xml.parse("<note>" + Xml.escape("Tom & Jerry <3") + "</note>");
assertEquals("Tom & Jerry <3", escaped.getDocumentElement().getTextContent());
Document cdata = Xml.parse("<note><![CDATA[Tom & Jerry <3]]></note>");
assertEquals("Tom & Jerry <3", cdata.getDocumentElement().getTextContent());
}Xml.escapein the project replaces the five characters. Lesson 7 shows what happens to a SOAP request when a value isn't escaped.
Reading XML in Java
Java's built-in parser turns a document into a tree of objects called the DOM, the Document Object Model. Each element is an Element you can ask for its attributes, its children and its text. The project's Xml.parse does the parsing, with two settings lessons 2 and 7 explain. The first test reads the response above:
@Test
@DisplayName("elements, attributes and text in a real response")
void readAResponse() {
Api.Response response = Api.get("/api/formats/xml");
assertEquals(200, response.status());
assertTrue(response.contentType().startsWith("application/xml"));
Document doc = response.xml();
assertEquals("users", doc.getDocumentElement().getTagName()); // the root element
NodeList users = doc.getElementsByTagName("user");
assertEquals(2, users.getLength());
Element alice = (Element) users.item(0);
assertEquals("1", alice.getAttribute("id")); // an attribute
assertEquals("Alice Johnson", alice.getElementsByTagName("name").item(0).getTextContent()); // element text
assertEquals(2, alice.getElementsByTagName("role").getLength()); // a list is repeated elements
// XML has no types: "true" is four characters of text until your code decides otherwise.
assertEquals("true", alice.getElementsByTagName("active").item(0).getTextContent());
}getDocumentElement()returns the root element.getElementsByTagName("user")finds everyuserelement, however deeply nested, in document order.getAttribute("id")reads an attribute; it returns an empty string if there isn't one.getTextContent()returns all the text inside an element, including the text of any child elements.
The last assertion compares active with the string "true", because that's what XML gives you. These DOM methods are fine for simple documents, but chaining them gets long quickly. Lesson 6 introduces XPath, which finds the same values in one line.
Check your understanding
1.How many root elements can an XML document have?Show answer
Exactly one. Everything else must be inside it.
2.A response contains <active>true</active>. What type is the value?Show answer
Text. XML has no booleans or numbers; the four characters only become a boolean if your code converts them, or a schema says what type the element holds.
3.Is <price currency=EUR>9.99</price> well-formed?Show answer
No. Attribute values must be quoted: currency="EUR".
4.A product name is Salt & Pepper. How is it written inside an element?Show answer
Salt & Pepper, or inside a CDATA section. A bare & makes the document not well-formed.
5.What's the difference between well-formed and valid?Show answer
Well-formed means the document follows XML's own rules, so it can be parsed. Valid means it also follows a schema describing which elements and values are allowed.
Exercise
Write a test that reads Bob from /api/formats/xmlsign in to see: his ID, his email, and how many roles he has. Then fetch /api/formats/rsssign in to see, an RSS feed, and print the title of each item using getElementsByTagName. What is the root element of an RSS document, and what content type does the API send it with?