skip to content

In REST Assured, how do you assert that an XML response body is valid against an XSD?

level: juniorimportance: should knowfreq 48%

answer

  1. REST Assured ships matchers of its own
  2. validate side, and no path argument
  3. RestAssuredMatchers.matchesXsdInClasspath
  4. matcher receives response.asString()

basics

~20 s

REST Assured ships matchesXsd and matchesXsdInClasspath in io.restassured.matcher.RestAssuredMatchers. Passed into then().body(Matcher) with no path argument, either one validates the entire response body against the schema. matchesXsd accepts a String, InputStream, Reader or File; matchesXsdInClasspath takes a classpath resource path.

solid answer

~40 s

`io.restassured.matcher.RestAssuredMatchers` is where REST Assured's own Hamcrest matchers live, and two of them cover XSD validation: `matchesXsd(...)`, with overloads taking a `String`, `InputStream`, `Reader` or `File`, and `matchesXsdInClasspath(String path)`, which loads the schema through the thread context classloader. You hand the result to the validate-side `then().body(Matcher)` overload — the one with **no** GPath argument — so REST Assured passes the whole response body, as a string, straight to the matcher. Internally the matcher builds its own `SchemaFactory` for the W3C XML Schema namespace, compiles your `.xsd` and runs a `Validator` over the body; it never consults `XmlConfig`, which governs `XmlPath` parsing instead. A classpath path may start with or without a leading slash, and the check is all-or-nothing: it says the document matched the schema, nothing about individual fields.

code

java · 9 lines
java
import static io.restassured.RestAssured.get;
import static io.restassured.matcher.RestAssuredMatchers.matchesXsdInClasspath;
import static org.hamcrest.Matchers.equalTo;

get("/sweep-schedule")
    .then()
        .statusCode(200)
        .body(matchesXsdInClasspath("sweep-schedule.xsd"))
        .body("sweepSchedule.booking[0].chimneyId", equalTo("CH-2081"));

go deeper

for a junior

Be ready to name the import — io.restassured.matcher.RestAssuredMatchers — and to write the assertion as then().body(matchesXsdInClasspath("...")) with no path argument.

for a middle

Explain why the no-path body(Matcher) overload hands over the raw body string, and why that means content type and parser registration are irrelevant to a schema check.

for a senior

Show judgment about what a schema assertion is worth in a suite: it catches shape drift cheaply but proves no values, so pair it with targeted field assertions rather than treating it as coverage.

for a principal

Own the question of where schema checking belongs at all — one assertion inside a functional test, a shared response specification, or a separate gate — and be able to defend the split you chose.

## The two matchers, and where they live REST Assured ships a handful of its own Hamcrest matchers in `io.restassured.matcher.RestAssuredMatchers`, and XSD validation is two of them. They are part of the core `rest-assured` artifact, so nothing extra has to be added to the build to use them. The XSD half of that class is: - `matchesXsd(String xsd)` — the schema already in memory as text - `matchesXsd(InputStream xsd)` — a stream you opened yourself - `matchesXsd(Reader xsd)` — a character stream - `matchesXsd(File xsd)` — a schema sitting on disk - `matchesXsdInClasspath(String path)` — a schema packaged alongside the tests All of them return `XmlXsdMatcher`, which is a `BaseMatcher<String>`. `matchesXsdInClasspath` is a thin wrapper: it opens a classpath stream and calls the `InputStream` overload. ## Attaching one to a response Schema validation goes through the **validate side** of the DSL, on the no-path `ValidatableResponse.body(Matcher)` overload: ```java get("/sweep-schedule").then().body(matchesXsdInClasspath("sweep-schedule.xsd")); ``` The distinction between the two `body(...)` overloads matters here. `body(String path, Matcher)` evaluates a GPath expression first and matches against the extracted value. `body(Matcher)` has no path at all, so REST Assured hands the matcher `response.asString()` — the entire body, unparsed. Two useful consequences fall out of that: - The schema check does **not** depend on the response's `Content-Type`. With no path to evaluate, REST Assured never builds a content parser, so a chimney-sweep endpoint that serves XML as `text/plain` still validates without a `RestAssured.registerParser(...)` call. - The matcher sees the body exactly as it arrived, prolog and whitespace included. ## What runs inside the matcher `XmlXsdMatcher.matches` is short, and it is the standard JAXP sequence: 1. Build a schema factory with `SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI)`. 2. Install an `LSResourceResolver` on it, if one was supplied through the matcher's `using(...)` hook. 3. Compile the schema with `factory.newSchema(...)` over whichever source you handed in. 4. Create a `Validator` from the compiled schema and run it over a `StreamSource` wrapping the response body. Note what is *absent* from that list: the matcher builds its own `SchemaFactory` and never reads REST Assured's `XmlConfig`. `XmlConfig.namespaceAware(...)`, `validating(...)`, `allowDocTypeDeclaration(...)` and `disableLoadingOfExternalDtd()` shape how `XmlPath` parses a document for GPath extraction. They have no effect whatsoever on `matchesXsd`, which is a frequent source of "I turned that on and nothing changed". ## Loading the schema from the classpath `matchesXsdInClasspath` resolves through the thread's context classloader and then falls back, so all of these find `src/test/resources/sweep-schedule.xsd`: - `matchesXsdInClasspath("sweep-schedule.xsd")` - `matchesXsdInClasspath("/sweep-schedule.xsd")` — the leading slash is stripped and retried - `matchesXsdInClasspath("schemas/sweep-schedule.xsd")` for a nested folder If the resource genuinely is not there the loader yields nothing and the failure surfaces from the schema factory rather than as a readable assertion message. Before you suspect the schema itself, check that the file is under a resources root that actually reaches the test classpath. ## Reusing a matcher instance The matcher holds on to whatever source you gave it, and three of the overloads wrap a one-shot stream: | Overload | What the matcher holds | Safe to reuse | |---|---|---| | `matchesXsd(String)` | `StreamSource` over a `StringReader` | no | | `matchesXsd(InputStream)` | `StreamSource` over that stream | no | | `matchesXsd(Reader)` | `StreamSource` over that reader | no | | `matchesXsd(File)` | the `File` object itself | yes | | `matchesXsdInClasspath(path)` | a stream opened per call | yes, per call | So call the factory method inside each test rather than hoisting one `XmlXsdMatcher` into a `static final` field, or hand it a `File` if you really want one shared instance. ## What the assertion actually proves For a chimney-sweep scheduling API, `get("/sweep-schedule").then().body(matchesXsdInClasspath("sweep-schedule.xsd"))` proves the whole document is structurally valid: declared elements only, correct nesting and ordering, cardinality respected, and simple types honoured — `sweepDate` really is an `xs:date`, `bookingRef` really matches its pattern. It proves nothing about any particular value: not that a booking exists for chimney `CH-2081`, not that the schedule is non-empty, not that the sweep was assigned to anyone. Those stay ordinary `body("path", matcher)` assertions, and you chain the schema check and the value checks in the same `then()` block. A useful way to split the labour on this endpoint: - The schema catches the service dropping `<flueType>` from every booking, or emitting `2026-13-40` where an `xs:date` is declared. - The schema does not catch an empty `<sweepSchedule/>`, or every sweep being scheduled against the wrong chimney. Because the check costs one line and says nothing about values, it is a good candidate for a shared response specification covering every XML endpoint in the suite, with the value assertions left where they belong, in the individual tests that care about them.

  • Does matchesXsd need the response to be served with an XML content type?
    No. `body(Matcher)` carries no GPath expression, so REST Assured never builds a content parser and simply hands the matcher `response.asString()`. A chimney-sweep endpoint returning XML under `text/plain` validates fine, and no `RestAssured.registerParser(...)` or `defaultParser` setting is required for the schema check itself.
  • Which XmlConfig settings change how matchesXsd parses the response?
    None of them. `XmlXsdMatcher` constructs its own `SchemaFactory` and `Validator` and never reads `RestAssuredConfig`. `XmlConfig.namespaceAware(...)`, `validating(...)`, `allowDocTypeDeclaration(...)` and `disableLoadingOfExternalDtd()` govern `XmlPath` parsing for GPath extraction, which is a separate code path from the schema matcher.

saying these in an interview costs you the question

  • Thinks a separate artifact is needed before matchesXsd resolves
  • Passes a GPath expression alongside the schema matcher
  • Believes XmlConfig settings configure how matchesXsd parses
  • Assumes the response must be registered as XML first
  • Reuses one matchesXsd(InputStream) matcher across many tests
  • Expects the schema check to also assert individual field values