skip to content

XML Schema and DTD

Validating an XML payload against an XSD or a DTD from inside the response assertion, and the resolver a multi-file schema needs before its own imports will resolve at all.

part ofREST Assuredoverview, primer and where to startread it →
on this pageshow

questions

4

In REST Assured, why does matchesXsd fail when the XSD has an xsd:import, and what fixes it?

level: seniorimportance: must knowfreq 42%

answer

  1. relative reference needs something to be relative to
  2. the stream carries no system id
  3. XmlXsdMatcher has a resolver hook
  4. using and with return a new matcher
  5. LSInput.getByteStream supplies the imported file

basics

~20 s

A schema handed to matchesXsd as a string or stream carries no base location, so a relative xsd:import or xsd:include cannot be found. Chain using(LSResourceResolver) or with(LSResourceResolver) on the returned XmlXsdMatcher and resolve each referenced file yourself.

solid answer

~40 s

`RestAssuredMatchers.matchesXsd(...)` returns an `XmlXsdMatcher`, and inside `matches` it compiles the schema with a `SchemaFactory`. When you hand it a `String`, `Reader` or `InputStream`, the resulting `StreamSource` has no system id, so the schema compiler has no base URI to resolve `schemaLocation="flue-types.xsd"` against — and it fails during compilation, before the response body is even looked at. `XmlXsdMatcher` exposes the fix: `using(LSResourceResolver)` and its alias `with(LSResourceResolver)`, both of which return a **new** matcher carrying the resolver, which is then set on the `SchemaFactory`. Your resolver's `resolveResource(type, namespaceURI, publicId, systemId, baseURI)` returns an `LSInput` whose `getByteStream()` supplies the imported file, usually loaded from the test classpath by `systemId`. Chain it directly on the factory call, because `using` does not mutate the receiver, and note that `matchesDtd` has no equivalent hook.

code

java · 23 lines
java
import org.w3c.dom.bootstrap.DOMImplementationRegistry;
import org.w3c.dom.ls.DOMImplementationLS;
import org.w3c.dom.ls.LSInput;
import org.w3c.dom.ls.LSResourceResolver;

import static io.restassured.RestAssured.get;
import static io.restassured.matcher.RestAssuredMatchers.matchesXsd;

ClassLoader cl = Thread.currentThread().getContextClassLoader();
DOMImplementationLS ls = (DOMImplementationLS) DOMImplementationRegistry
        .newInstance().getDOMImplementation("LS");

LSResourceResolver fromClasspath = (type, ns, publicId, systemId, baseUri) -> {
    LSInput imported = ls.createLSInput();
    imported.setSystemId(systemId);
    imported.setByteStream(cl.getResourceAsStream(systemId));
    return imported;
};

get("/sweep-schedule")
    .then()
        .body(matchesXsd(cl.getResourceAsStream("sweep-schedule.xsd"))
                .using(fromClasspath));

go deeper

for a junior

Recognise that a schema split across files behaves differently from a single one, and that the failure arrives while the schema is being compiled rather than while the response is being checked.

for a middle

Explain the mechanism: a StreamSource built from a string or stream has no system id, so a relative schemaLocation has no base URI, and the resolver supplies the bytes instead.

for a senior

Be ready to write the LSResourceResolver in the interview and to say why it goes in a shared helper: one resolver keyed on systemId or target namespace, reused across the suite rather than copied per test.

for a principal

Own the tradeoff between resolving the published multi-file schema faithfully and flattening a copy for tests — the flattened copy is cheaper and silently drifts from the contract it claims to check.

## The symptom Split a chimney-sweep schedule schema across three files — `sweep-schedule.xsd` importing `flue-types.xsd` and `common-types.xsd` — and an assertion that worked against a single-file schema stops working: ```java get("/sweep-schedule").then().body(matchesXsd(schemaAsString)); ``` What you get is not a body-mismatch report. It is a schema **compilation** failure, raised before the response body is examined at all, complaining that the schema document at `flue-types.xsd` could not be read or that a type in the `flues` namespace cannot be resolved. ## Why the import cannot resolve `XmlXsdMatcher.matches` does four things: 1. `SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI)` 2. `factory.setResourceResolver(resolver)` — only if one was supplied 3. `factory.newSchema(source)` — compile the schema 4. `schema.newValidator().validate(...)` — run it over the response body Step 3 is where a multi-file schema dies. The `String`, `Reader` and `InputStream` overloads wrap what you passed in a `StreamSource` with **no system id**. `xsd:import schemaLocation="flue-types.xsd"` is a *relative* reference, and a relative reference needs a base URI to be relative *to*. Without a system id there is no base URI, so the compiler has nowhere to look. A few things that feel like they should help, and do not: - `matchesXsdInClasspath(...)` is no safer — it opens a bare classpath `InputStream` and calls the `InputStream` overload, so it has exactly the same missing base URI. - Putting the imported files next to the main one on the classpath changes nothing, because nothing tells the compiler that the classpath is the place to look. - `XmlConfig` settings are irrelevant here; the matcher builds its own factory and never reads them. ## The hook that fixes it `XmlXsdMatcher` — and only `XmlXsdMatcher` — carries the escape hatch: - `using(LSResourceResolver resourceResolver)` - `with(LSResourceResolver resourceResolver)`, an alias that delegates straight to `using` Both return a **new** `XmlXsdMatcher` over the same schema source with the resolver attached; neither mutates the matcher you called it on. Two practical consequences follow. First, chain the call directly on the factory method — `matchesXsd(source).using(resolver)` — rather than calling `using` on a variable and throwing the result away. Second, if you do split it across statements, declare the variable as `XmlXsdMatcher`, not `Matcher<String>`, or the `using` call will not compile. ## Writing the resolver `org.w3c.dom.ls.LSResourceResolver` has one method: ```java LSInput resolveResource(String type, String namespaceURI, String publicId, String systemId, String baseURI); ``` The pieces you actually use: - `systemId` is the raw `schemaLocation` text — `flue-types.xsd` — which is normally the key you look the file up by. - `namespaceURI` is the imported schema's target namespace, a more robust key when file names are not stable. - The return value is an `LSInput`; supply the bytes through `setByteStream(...)` or the characters through `setCharacterStream(...)`, and set the system id so nested references have somewhere to hang off. - Returning `null` means "fall back to default resolution", which is the behaviour that already failed, so a resolver that returns `null` for the file you need fixes nothing. The library's own integration test builds exactly this: a resolver whose `LSInput.getByteStream()` loads `systemId` through the thread context classloader, attached with `.with(...)`. ## What has no hook at all | Matcher | Declared return type | Resolver hook | |---|---|---| | `matchesXsd(...)` | `XmlXsdMatcher` | `using(...)` / `with(...)` | | `matchesXsdInClasspath(...)` | `XmlXsdMatcher` | same | | `matchesDtd(...)` | `Matcher<String>` | none | | `matchesDtdInClasspath(...)` | `Matcher<String>` | none | That asymmetry is deliberate in effect if not in intent: the DTD matcher hands its work to a validating `DocumentBuilderFactory` with no resolver plumbed through, so a DTD that pulls in another file by a relative system id has nothing to resolve it with. If your document contract is genuinely multi-file, XSD is the side of this leaf that can be made to work. ## Keeping it boring If you never want to write an `LSResourceResolver` at all, the alternatives are structural: - Keep the imported schemas in one classpath folder and key the resolver on the file name once, in a shared helper, rather than per test. - Hand `matchesXsd` a `File` whose parent directory holds the imports, so the source carries a real system id and relative locations resolve on their own. - Or flatten the schema for test purposes, accepting that the flattened copy can drift from the one the service publishes — which is usually the reason teams write the resolver instead.

  • What is the difference between XmlXsdMatcher.using and XmlXsdMatcher.with?
    None in behaviour — `with(LSResourceResolver)` delegates directly to `using(LSResourceResolver)`. Both construct a new `XmlXsdMatcher` over the same schema source with the resolver attached and return it, leaving the receiver untouched. `with` exists purely so the chained call reads naturally: `matchesXsd(schema).with(resolver)`.
  • Can you attach a resolver to matchesDtd when a DTD pulls in another file?
    No. `matchesDtd` is declared to return `Matcher<String>` and the underlying matcher exposes no resolver hook at all — it hands the work to a validating `DocumentBuilderFactory` with nothing plumbed in. A DTD split across files has no supported resolution path here; consolidate it, or express the contract as an XSD instead.
  • Why does assigning matchesXsd's result to Matcher<String> break the fix?
    `using(...)` and `with(...)` are declared on `XmlXsdMatcher`, not on `Matcher`. Widening the reference to `Matcher<String>` hides them, so the resolver call will not compile. Either chain straight off the factory method or keep the variable typed as `XmlXsdMatcher`.

A relative schemaLocation is a flat number with no street name on it. Handing the schema in as a bare stream throws the street away, and the resource resolver is the porter who knows which flat each number means.

saying these in an interview costs you the question

  • Thinks matchesXsdInClasspath resolves relative imports automatically
  • Expects an XmlConfig setting to make the import resolve
  • Calls using(...) and discards the returned matcher
  • Believes matchesDtd accepts the same resolver hook
  • Blames the response body for a schema compilation failure
  • Returns null from resolveResource for the file that is missing
open as a page

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

level: juniorimportance: should knowfreq 48%

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.

open as a page

In REST Assured, what does matchesDtd do to the response body before it validates it?

level: middleimportance: should knowfreq 33%

basics

~20 s

matchesDtd parses the response into a DOM, then writes your DTD to a temporary file. It re-serialises the document with a DOCTYPE pointing at that file and re-parses it through a validating DocumentBuilder. Any warning or error throws.

open as a page

In REST Assured, why does a failing matchesXsd assertion throw SAXParseException, not an AssertionError?

level: middleimportance: should knowfreq 38%

basics

~20 s

XmlXsdMatcher.matches runs a JAXP Validator, which throws on the first schema violation rather than returning false. REST Assured calls the matcher without catching, so that SAXParseException propagates. You read the validator's own message instead of a counted-expectations report.

open as a page