XML Response Handling with XmlPath

8 min read

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 function

XPath 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. Use equalTo("1"), or extract via xmlPath().getInt("user.id") and compare in Java.
  • Forgetting Accept: application/xml. Many APIs serve JSON by default and only return XML when asked. Without the Accept header, 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 is user.roles.role, not user.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.

  1. Set RestAssured.baseURI = "https://www.w3schools.com" and write getNoteXml() against /xml/note.xml. Use accept(ContentType.XML) and assert the response status is 200.
  2. Field assertions. Assert body("note.to", equalTo("Tove")), body("note.from", equalTo("Jani")), body("note.body", containsString("weekend")).
  3. Extract values. Use xmlPath().getString("note.heading") and print it. Confirm the value is "Reminder".
  4. Try a list endpoint. GET https://www.w3schools.com/xml/cd_catalog.xml. Assert body("CATALOG.CD.size()", equalTo(26)) (or whatever the current number is — read the response first).
  5. XPath check. Add body(hasXPath("//CD[1]/TITLE", equalTo("Empire Burlesque"))). Note how the XPath form pairs with Hamcrest matchers.
  6. Extract a list. Use xmlPath().getList("CATALOG.CD.ARTIST") to capture every artist into List<String>. Assert it contains "Bob Dylan".
  7. 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.
  8. Stretch: find or build an XSD that matches cd_catalog.xml. Validate with matchesXsd. 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.

// tip to track lessons you complete and pick up where you left off across devices.