skip to content

In REST Assured, how do you assert a JSON response against a schema file on the classpath?

level: juniorimportance: must knowfreq 58%

answer

  1. not in the core jar
  2. a second io.rest-assured coordinate
  3. package io.restassured.module.jsv
  4. body() with no path argument
  5. classpath-root relative, no slash

basics

~20 s

REST Assured's schema matcher ships in a separate artifact, io.rest-assured:json-schema-validator, declared alongside rest-assured. Statically import matchesJsonSchemaInClasspath from io.restassured.module.jsv.JsonSchemaValidator and pass it to the no-path body() overload. It validates the entire response body as one assertion, not a single field.

solid answer

~50 s

Schema validation is not in the core REST Assured jar. Declare `io.rest-assured:json-schema-validator` next to `rest-assured`; it wraps `com.github.java-json-tools:json-schema-validator`, which does the validating. Then statically import from `io.restassured.module.jsv.JsonSchemaValidator` and give the matcher to the `body(Matcher)` overload that takes **no path**, as in `.then().statusCode(200).body(matchesJsonSchemaInClasspath("schemas/carbon-survey-schema.json"))` against a peatland carbon-survey endpoint. The argument is resolved through the thread context class loader, so it is relative to the classpath root with no leading slash. Sibling overloads take the schema as a `File`, `URL`, `URI`, `InputStream`, `Reader`, or a `String` holding the schema document itself. The result is an ordinary Hamcrest `TypeSafeMatcher<String>` over the raw body, so the same call also works in a bare `assertThat(json, matchesJsonSchemaInClasspath(...))` with no REST Assured request at all. Neither artifact depends on the other, so both coordinates have to be listed in the build file explicitly.

code

java · 10 lines
java
import static io.restassured.RestAssured.given;
import static io.restassured.module.jsv.JsonSchemaValidator.matchesJsonSchemaInClasspath;

given()
    .pathParam("surveyId", "PEAT-2291")
.when()
    .get("/surveys/{surveyId}")
.then()
    .statusCode(200)
    .body(matchesJsonSchemaInClasspath("schemas/carbon-survey-schema.json"));

go deeper

for a junior

Be ready to name the extra dependency and the static import, and to show the matcher going into body() with no path argument. Knowing the classpath path is root-relative saves you the most common first failure.

for a middle

Explain the split: REST Assured supplies the matcher seam, the wrapped library supplies the validation. Be able to list the overloads and say which ones load the schema eagerly and which defer it to match time.

for a senior

Show judgment about what belongs in the schema versus in targeted value assertions, and about ordering status and content-type expectations ahead of the schema check so failures stay readable in CI.

for a principal

Own the policy question: where schema documents live, who updates them when the API changes, and how much a functional schema assertion is allowed to stand in for a real compatibility gate.

## The matcher does not ship with `rest-assured` The core REST Assured artifact has no JSON Schema support. The matcher lives in a **second artifact under the same group id**, `io.rest-assured:json-schema-validator`, which you declare *in addition to* `io.rest-assured:rest-assured` — neither coordinate pulls the other in. That module is deliberately thin: it wraps `com.github.java-json-tools:json-schema-validator` (the library that actually performs validation), adds Guava and Hamcrest, and exposes one public matcher class, `io.restassured.module.jsv.JsonSchemaValidator`, plus `JsonSchemaValidatorSettings` and `JsonSchemaValidationException`. The practical consequence of that split is that **REST Assured owns none of the schema semantics**. Which drafts are understood, how a violation is worded, how `$ref` is resolved — all of that belongs to the wrapped library and is configured through its `JsonSchemaFactory`. REST Assured owns only the seam: a Hamcrest matcher you can hand to `body(...)`. ## Wiring it into the given/when/then chain The class is built for static import, and the project docs recommend importing all of `io.restassured.module.jsv.JsonSchemaValidator.*`. Against a peatland carbon-survey API: ```java given().pathParam("surveyId", "PEAT-2291") .when().get("/surveys/{surveyId}") .then().statusCode(200) .body(matchesJsonSchemaInClasspath("schemas/carbon-survey-schema.json")); ``` Notice **which `body` overload** that is. `ValidatableResponseOptions` declares both `body(String path, Matcher<?>, Object...)` and `body(Matcher<?>, Matcher<?>...)`. A schema matcher goes to the second, path-less form, because it validates the whole document. If you supply a path, REST Assured evaluates that GPath expression first and hands the matcher only the extracted value — which is not what a schema describes. ## The six ways to name a schema - `matchesJsonSchemaInClasspath(String)` — a **classpath resource path**, resolved through `Thread.currentThread().getContextClassLoader().getResource(...)`. - `matchesJsonSchema(String)` — the schema **document itself as text**. This is the overload people misuse: a file path handed here is parsed as JSON and fails. - `matchesJsonSchema(File)` — a filesystem location, loaded immediately. - `matchesJsonSchema(Reader)` — any character stream. - `matchesJsonSchema(InputStream)` — delegates to the `Reader` form by wrapping the stream. - `matchesJsonSchema(URL)` and `matchesJsonSchema(URI)` — the `URI` form converts to a `URL` first. Loading is **eager for `String`, `Reader` and `File`** (the document is parsed while the matcher is built) and **lazy for `URL` and `URI`** — and since `matchesJsonSchemaInClasspath` funnels into the `URL` overload, a classpath schema is only read when the assertion runs. ## Where a classpath schema actually has to sit 1. **Root-relative, no leading slash.** The lookup goes through a `ClassLoader`, whose paths are already root-relative; `/schemas/...` will not resolve. 2. **On the *test* classpath.** For Maven and Gradle that means `src/test/resources/schemas/carbon-survey-schema.json`, and the path you pass omits the `src/test/resources` prefix. 3. **Actually copied into the build output** — `target/test-classes` or `build/resources/test`. A resource excluded by a filtering rule is invisible to the lookup even though the file is in the repository. 4. **Case-exact.** Lookups inside a jar, and on a Linux CI agent, are case-sensitive even when a developer's laptop filesystem is not. ## What the matcher is handed `JsonSchemaValidator extends TypeSafeMatcher<String>`. When you pass it to the path-less `body(...)`, REST Assured calls `matcher.matches(response.asString())` — the response body **as raw text**, exactly as it arrived on the wire. Nothing is routed through the JSON path engine, and the response content type is not consulted. The matcher parses that text itself and validates the resulting node. That also explains a feature worth knowing: because the module depends on Hamcrest rather than on `rest-assured`, the matcher works with no REST Assured call at all. Given any JSON string, `assertThat(json, matchesJsonSchemaInClasspath("schemas/carbon-survey-schema.json"))` is a valid plain-Hamcrest assertion, which is handy when a message-driven test has a payload but no HTTP response. ## What one passing assertion proves - **This response matched this document, at this moment.** It is a functional assertion inside one test, not a compatibility gate over the API's history. - **It covers the whole payload at once**, which is exactly why it survives field additions that a long list of `body("path", equalTo(...))` assertions would not — provided the schema permits them. - **It says nothing about values you care about specifically.** A `carbonStockTonnesPerHa` of `-9999` satisfies a schema that only says `number`, so keep targeted value assertions beside the schema check. - **It is only as strong as the schema.** A permissive document that declares three properties and allows everything else will pass almost any response. A good habit is to assert the cheap things first — `statusCode(200)`, then the content type, then the schema — so a wrong-shaped response fails on something readable before it reaches the validator.

  • Does the json-schema-validator module pull rest-assured in, or the other way round?
    Neither. The module's compile dependencies are the wrapped `com.github.java-json-tools:json-schema-validator`, Guava and Hamcrest — it does not depend on `rest-assured`, and `rest-assured` does not depend on it. You declare both coordinates yourself. That independence is also why the matcher works standalone: `assertThat(json, matchesJsonSchemaInClasspath(...))` is valid in a project with no REST Assured on the classpath.
  • What does the path-less body(Matcher) overload pass to the matcher — the parsed payload or the raw text?
    The raw text. With no path argument REST Assured calls `matcher.matches(response.asString())`, so the matcher receives the body exactly as it arrived. `JsonSchemaValidator` is a `TypeSafeMatcher<String>` and parses that text itself before validating. The response content type and the GPath engine play no part, which is why a non-JSON body blows up inside the matcher rather than failing an expectation.

The core jar is the camera and the schema module is a lens you buy separately: it bolts onto the same mount, but nothing in the box brings it along.

saying these in an interview costs you the question

  • Thinks matchesJsonSchemaInClasspath comes with the rest-assured dependency
  • Passes a leading slash or an src/test/resources prefix to the classpath overload
  • Gives matchesJsonSchema(String) a file path instead of the schema text
  • Puts the schema path in body()'s first argument, as if it were a GPath
  • Believes REST Assured implements the JSON Schema drafts itself
  • Treats one passing schema assertion as an API compatibility guarantee