skip to content

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