In REST Assured, why does a failing matchesXsd assertion throw SAXParseException, not an AssertionError?
answer
- the matcher never returns false
- JAXP throws when no error handler is set
- no try/catch around matches(...)
- the catch only fires failure listeners
- one violation per run, not a list
basics
~20 sXmlXsdMatcher.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.
solid answer
~50 sMost REST Assured expectations fail by returning `false` from a Hamcrest matcher; the library then collects the mismatches and throws one `AssertionError` reading `N expectations failed.` with a description per failure. The schema matchers are the exception. `XmlXsdMatcher.matches` calls `Validator.validate(...)` with no `ErrorHandler` installed, so JAXP **throws** the first `SAXParseException` it hits instead of reporting a boolean. REST Assured's body-matcher code calls `matcher.matches(response.asString())` with no `try`/`catch`, and the enclosing validation block catches `Throwable` only to fire the configured failure listeners before rethrowing it unchanged. So the SAX exception reaches your test verbatim. The DTD matcher behaves the same way, through an error handler that rethrows on `warning`, `error` and `fatalError` alike. Practically that means you read a parser message with a line and column rather than a mismatch report, and no later expectation in the same chain is evaluated at all.
code
java · 13 linesimport org.xml.sax.SAXParseException;
import static io.restassured.RestAssured.get;
import static io.restassured.matcher.RestAssuredMatchers.matchesXsdInClasspath;
import static org.junit.jupiter.api.Assertions.assertThrows;
SAXParseException failure = assertThrows(SAXParseException.class, () ->
get("/sweep-schedule")
.then()
.body(matchesXsdInClasspath("sweep-schedule.xsd")));
System.out.println(failure.getLineNumber() + ":" + failure.getColumnNumber()
+ " " + failure.getMessage());go deeper
Recognise the failure when you see it: a schema check that fails shows a SAXParseException naming an element, not REST Assured's usual expectations-failed message.
Explain the mechanism — the JAXP validator throws rather than returning false, and REST Assured calls the matcher without catching, so the exception escapes the normal mismatch reporting.
Show how you make this diagnosable in CI: capture the body alongside the exception, expect one violation per run, and know when to validate outside the matcher to collect them all.
Take a position on whether a check that reports one violation and bypasses the suite's normal failure reporting belongs inside functional tests, and what the alternative costs the team.
## What you expect to see A normal REST Assured failure looks like this: a Hamcrest matcher returns `false`, the library records a mismatch, and at the end of the `then()` block it throws a single `AssertionError` whose message begins `2 expectations failed.` and then describes each one, with any `onFailMessage(...)` text appended. The schema matchers do not participate in that machinery, and the first time a chimney-sweep schedule stops matching `sweep-schedule.xsd` you find out the hard way — the stack trace shows `org.xml.sax.SAXParseException` with a message like `Cannot find the declaration of element 'booking'.` and nothing about expectations at all. ## What the matcher actually does `XmlXsdMatcher.matches` compiles the schema and then runs the validator: ```java def validator = schema.newValidator(); return validator.validate(new StreamSource(new StringReader(item))) == null; ``` Two details make that a throwing path rather than a boolean one: - `Validator.validate(Source)` returns nothing. When no `ErrorHandler` has been set on it, JAXP's contract is to **throw** the first error it encounters, so an invalid document never reaches the comparison. - Consequently the matcher only ever returns `true` or throws. It has no false branch to report, and its `describeTo` says only `the supplied XSD`, so even if it did return `false` you would learn nothing from the description. ## Why REST Assured re-throws it unchanged The body assertion is evaluated as a bare call: ```java } else if (!matcher.matches(response.asString())) { ``` There is no `try`/`catch` around it. One level up, the response specification wraps the whole validation pass in `catch (Throwable e)` — but only so it can fire the `FailureConfig` failure listeners, after which it does `throw e`. The `AssertionError` carrying `N expectations failed.` is built *after* that block, from the list of recorded mismatches, so an exception thrown mid-pass never reaches it. That single design fact explains everything you observe: - The exception type is `SAXParseException`, not `AssertionError`. REST Assured's own integration tests assert exactly that for both a failing XSD and a failing DTD. - No other expectation in the same `then()` chain is evaluated — validation aborts at the throw. - Any `onFailMessage(...)` you set never appears, because it is only appended to the `AssertionError` that is never built. - Failure listeners registered through `FailureConfig` **do** still fire, since that is the one thing the `catch` block does before rethrowing. ## What that costs you, and what it buys The cost is a stack trace instead of a report: - You get one violation per run. Schema validation stops at the first error, so fixing it can uncover a second and a third on successive runs. - `MatcherConfig.errorDescriptionType(...)` changes how mismatches are *described*, and a thrown exception is not a mismatch, so switching it makes no difference here. - Some runners and reporters treat a non-`AssertionError` as an error rather than a failure, which can change how the result is bucketed in a report. The benefit is that the message is genuinely specific. A `SAXParseException` carries `getLineNumber()`, `getColumnNumber()` and the parser's own diagnosis — `Cannot find the declaration of element 'booking'.` names the exact element the schema does not admit, which is far more useful than "the body did not match the supplied XSD". One wrinkle worth knowing: those messages come from the JAXP implementation and are **locale-sensitive**. REST Assured's own tests set the default locale to English before asserting on the text, and any assertion you write against a message string should do the same or match on structure instead. ## Reading a failure quickly When a schema assertion blows up in a chimney-sweep suite, work through it in this order: 1. Read the exception type. `SAXParseException` on the response means the document is invalid; a failure while the schema itself is being compiled means the schema or one of its imports could not be read. 2. Read the message and the line and column. `Cannot find the declaration of element 'flueLining'.` usually means the service added a field the schema has not caught up with. 3. Log the body so you can see what was actually returned, since the exception carries a position but not the payload. 4. If you need every violation rather than the first, validate the captured body yourself with a `Validator` and a collecting `ErrorHandler` — the matcher gives you no hook for that. ## The DTD side behaves the same way `matchesDtd` reaches the same outcome by a different route: it installs an error handler whose `warning`, `error` and `fatalError` methods each rethrow the `SAXParseException` they receive, and its `matches` returns `true` if nothing threw. So the DTD matcher is, if anything, stricter — a warning aborts the assertion — and it surfaces the same exception type, with messages such as `Element type "booking" must be declared.`
- Does onFailMessage(...) text appear when a schema assertion fails?No. `onFailMessage(...)` is appended to the `AssertionError` REST Assured builds from the list of recorded mismatches, and a thrown `SAXParseException` short-circuits that path entirely — the error is never constructed. Failure listeners registered through `FailureConfig` still fire, because the enclosing catch runs them before rethrowing.
- How would you get every schema violation instead of only the first?Not through the matcher. Extract the body first, then validate it yourself with a JAXP `Validator` on which you have set a collecting `ErrorHandler` whose `error` method records rather than throws. That reports every violation in one pass; `matchesXsd` installs no handler, so JAXP throws at the first one.
saying these in an interview costs you the question
- Expects the usual N expectations failed message from a schema check
- Thinks the matcher returns false on an invalid document
- Believes MatcherConfig can improve the schema failure output
- Assumes later expectations in the same then() chain still run
- Asserts on the parser's message without pinning the locale