JSON dominates the modern API landscape, but XML is alive and well in three corners: SOAP services, banking and financial APIs, and government integrations. If your test suite ever has to talk to one of those, you'll be glad Rest Assured handles XML with the same fluent chain you already know — just with XmlPath instead of JsonPath for navigation and hasXPath for the rare case where XPath syntax is more natural. This lesson is the XML toolkit, kept short on purpose: enough to be productive when you need it, not so much that you forget it before you do.
Asking for XML in the response
A polite request advertises its preferences. Accept: application/xml tells the server you want XML; many APIs honour it via content negotiation:
given()
.accept(ContentType.XML)
.when()
.get("/legacy/users/1")
.then()
.statusCode(200)
.contentType(ContentType.XML)
.body("user.name", equalTo("Alice"))
.body("user.email", equalTo("alice@test.com"));The body(path, matcher) chain looks identical to its JSON cousin. Rest Assured detects that the response is XML and runs the path through XmlPath instead of JsonPath. You don't switch dialects.
A reference XML response
<user>
<id>1</id>
<name>Alice</name>
<email>alice@test.com</email>
<roles>
<role>admin</role>
<role>tester</role>
</roles>
<address>
<city>London</city>
<postcode>SW1A 1AA</postcode>
</address>
</user>The shape: a root element (user) with text-content children, repeated children (roles/role), and nested elements (address). Every navigation example below works against this body.
Pulling out values with XmlPath
xmlPath() mirrors jsonPath() — typed extractors, dotted paths:
import io.restassured.response.Response;
Response response = given()
.accept(ContentType.XML)
.when()
.get("/legacy/users/1");
String name = response.xmlPath().getString("user.name"); // "Alice"
int id = response.xmlPath().getInt("user.id"); // 1
String city = response.xmlPath().getString("user.address.city"); // "London"
List<String> roles = response.xmlPath().getList("user.roles.role"); // ["admin", "tester"]Repeated child elements (<role> appearing more than once inside <roles>) come back as a List — XmlPath does the implicit aggregation. Single elements come back as a single value. Same convention as JsonPath; different underlying tree.
Asserting in the chain
.then()
.statusCode(200)
.body("user.name", equalTo("Alice"))
.body("user.id", equalTo("1")) // note: string, not int
.body("user.roles.role", hasItems("admin", "tester"))
.body("user.address.city", equalTo("London"))
.body("user.roles.role.size()", equalTo(2));One quirk: in body chain assertions, XML element values come through as Strings (XML has no native numeric type). equalTo("1") not equalTo(1). When you need typed values, use xmlPath().getInt(...) and assert in Java.
When to reach for XPath instead
For most cases, XmlPath's dotted notation is the cleaner path. But XPath is a richer language — it has predicates, axes, position-based selection — and Rest Assured supports it via hasXPath:
import static org.hamcrest.Matchers.hasXPath;
// (note: hasXPath comes from Hamcrest, not from a Rest Assured-specific import)
.body(hasXPath("//user/name", equalTo("Alice")))
.body(hasXPath("//user/roles/role[1]", equalTo("admin"))) // first role
.body(hasXPath("//user/roles/role[text()='admin']")) // element with this text exists
.body(hasXPath("count(//user/roles/role)", equalTo("2"))) // XPath count functionXPath is what you reach for when:
- You want a position (
role[1]for the first,role[last()]for the last). - You want a predicate-on-text (
role[text()='admin']). - You're matching against a deeply nested document and don't want to spell every step (
//user/...walks down from anywhere).
For straight field lookups, xmlPath().getString(...) is shorter. Pick by clarity at the call site.
XSD validation — the schema cousin
Just as JSON Schema validates JSON shape, XSD validates XML shape. Rest Assured supports XSD validation via matchesXsd:
import static io.restassured.matcher.RestAssuredMatchers.matchesXsd;
import java.io.File;
given()
.accept(ContentType.XML)
.when()
.get("/legacy/users/1")
.then()
.statusCode(200)
.body(matchesXsd(new File("src/test/resources/schemas/user.xsd")));A small user.xsd for the example response above:
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="user">
<xs:complexType>
<xs:sequence>
<xs:element name="id" type="xs:int"/>
<xs:element name="name" type="xs:string"/>
<xs:element name="email" type="xs:string"/>
<xs:element name="roles">
<xs:complexType>
<xs:sequence>
<xs:element name="role" type="xs:string" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>Same role as JSON Schema in the previous lesson — define the shape once, validate every response. Most XML APIs publish their XSD; if yours does, treat it as the contract test it deserves to be.
SOAP responses — the special case
SOAP wraps the actual payload in a Body envelope:
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<getUserResponse xmlns="http://example.com/users">
<user>
<id>1</id>
<name>Alice</name>
</user>
</getUserResponse>
</soap:Body>
</soap:Envelope>The path navigates through the envelope: Envelope.Body.getUserResponse.user.name. Rest Assured strips the namespace prefixes (soap:) by default; if your API needs them preserved, configure with XmlPathConfig.xmlPathConfig().declaredNamespace(...). Most teams find the default behaviour matches what they want.
Two formats, the same task
JSON vs XML — same data, different navigation syntax
JSON
{ "user": { "name": "Alice", "roles": ["admin", "tester"] } }
jsonPath().getString("user.name")
jsonPath().getList("user.roles")
.body("user.name", equalTo("Alice"))
Schema: JSON Schema (.json)
Used by: most modern REST APIs
XML
<user><name>Alice</name><roles><role>admin</role><role>tester</role></roles></user>
xmlPath().getString("user.name")
xmlPath().getList("user.roles.role")
.body("user.name", equalTo("Alice"))
Schema: XSD (.xsd) or hasXPath
Used by: SOAP, legacy/banking/government APIs
The headline: the DSL is identical. Rest Assured picks the right parser by content type, the dotted-path expressions feel similar, and the same body(path, matcher) chain works. The differences are surface — XML values as strings, repeated children flattened to lists, and hasXPath for the position/predicate cases that XPath does better.
A complete XML test
@Test
public void getLegacyUserReturnsValidXml() {
given()
.accept(ContentType.XML)
.when()
.get("/legacy/users/1")
.then()
.statusCode(200)
.contentType(ContentType.XML)
.body("user.id", equalTo("1"))
.body("user.name", equalTo("Alice"))
.body("user.email", containsString("@"))
.body("user.roles.role", hasItems("admin", "tester"))
.body("user.roles.role.size()", equalTo(2))
.body(hasXPath("//user/address/city", equalTo("London")))
.body(matchesXsd(new File("src/test/resources/schemas/user.xsd")));
}Status, content type, three field assertions, an array assertion, an XPath check, and a full XSD validation in one chain. Same structural rhythm as a good JSON test.
⚠️ Common mistakes
- Asserting
equalTo(1)on an XML field. XML doesn't carry types in its body —<id>1</id>is the string"1"to XmlPath. UseequalTo("1"), or extract viaxmlPath().getInt("user.id")and compare in Java. - Forgetting
Accept: application/xml. Many APIs serve JSON by default and only return XML when asked. Without theAcceptheader, your "XML test" gets a JSON body, the body chain expressions don't match, and the failure is confusing. Always state the format you want. - Treating
<roles>as a list.<roles>is the container;<role>is the repeated element. The path isuser.roles.role, notuser.roles. This trips everyone the first time — it's the most common XmlPath mistake.
🎯 Practice task
XML test endpoints are rare in the public internet, but a few exist. The W3Schools tutorial XML at https://www.w3schools.com/xml/note.xml is a solid sandbox for the dotted-path style. 20–30 minutes.
- Set
RestAssured.baseURI = "https://www.w3schools.com"and writegetNoteXml()against/xml/note.xml. Useaccept(ContentType.XML)and assert the response status is 200. - Field assertions. Assert
body("note.to", equalTo("Tove")),body("note.from", equalTo("Jani")),body("note.body", containsString("weekend")). - Extract values. Use
xmlPath().getString("note.heading")and print it. Confirm the value is"Reminder". - Try a list endpoint. GET
https://www.w3schools.com/xml/cd_catalog.xml. Assertbody("CATALOG.CD.size()", equalTo(26))(or whatever the current number is — read the response first). - XPath check. Add
body(hasXPath("//CD[1]/TITLE", equalTo("Empire Burlesque"))). Note how the XPath form pairs with Hamcrest matchers. - Extract a list. Use
xmlPath().getList("CATALOG.CD.ARTIST")to capture every artist intoList<String>. Assert it contains"Bob Dylan". - Mix with JSON. Add a JSON test in the same class against any JSON API. Note how the
given()/when()/then()rhythm doesn't change — just the path syntax does. - Stretch: find or build an XSD that matches
cd_catalog.xml. Validate withmatchesXsd. Force a failure by tightening one type. Restore.
That's response validation covered — JsonPath, Hamcrest, JSON Schema, and XML. Chapter 4 turns to authentication: basic, OAuth 2.0, token management, and the role-based access tests every secured API needs.