REST Assured JSON Schema Validation: A Complete Guide
Field-by-field assertions work fine when an API has three fields. When your API returns a complex nested object with 30 fields, writing individual body("field", equalTo(...)) calls for each one is tedious, fragile, and easy to get wrong. JSON Schema validation solves this: define the expected shape of a response once, then validate every test response against it automatically.
REST Assured has built-in support for JSON Schema validation. This guide shows you how to set it up, write schemas, and use them effectively in your test suite.
What JSON Schema Validation Catches
Before diving into implementation, let's be clear about what schema validation does and doesn't do.
It does catch:
- Missing required fields
- Fields with the wrong type (string where you expected integer)
- Values outside allowed ranges or patterns
- Extra unexpected fields (if you configure
additionalProperties: false) - Null values on non-nullable fields
- Array items with the wrong structure
It doesn't catch:
- Wrong values that match the correct type (e.g., wrong user ID that's still an integer)
- Business logic errors
- Missing data that isn't in the schema
Schema validation is a contract check — it verifies the shape of the response, not the semantic correctness of the data. Use it alongside value assertions, not instead of them.
Setup
Add the JSON Schema validator module to your Maven dependencies:
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>json-schema-validator</artifactId>
<version>5.4.0</version>
<scope>test</scope>
</dependency>Add the static import to your test class:
import static io.restassured.module.jsv.JsonSchemaValidator.*;Your First Schema
Create a schema file in src/test/resources/schemas/. JSON Schema files are plain JSON:
src/test/resources/schemas/user-response.json:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "User Response",
"type": "object",
"required": ["id", "email", "name", "createdAt"],
"properties": {
"id": {
"type": "integer",
"minimum": 1
},
"email": {
"type": "string",
"format": "email"
},
"name": {
"type": "string",
"minLength": 1,
"maxLength": 255
},
"role": {
"type": "string",
"enum": ["admin", "user", "viewer"]
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"avatarUrl": {
"type": ["string", "null"],
"format": "uri"
}
},
"additionalProperties": false
}Use this schema in a REST Assured test:
import static io.restassured.RestAssured.*;
import static io.restassured.module.jsv.JsonSchemaValidator.*;
import static org.hamcrest.Matchers.*;
@Test
void getUserResponse_matchesSchema() {
given()
.when()
.get("/api/users/1")
.then()
.statusCode(200)
.body(matchesJsonSchemaInClasspath("schemas/user-response.json"));
}matchesJsonSchemaInClasspath looks for the file on the classpath — src/test/resources/schemas/ is on the classpath by default in Maven projects, so you don't need to include src/test/resources/ in the path.
Combining Schema Validation with Value Assertions
Schema validation and value assertions work together in the same then() block:
@Test
void getUserResponse_correctSchemaAndValues() {
given()
.when()
.get("/api/users/1")
.then()
.statusCode(200)
.body(matchesJsonSchemaInClasspath("schemas/user-response.json"))
.body("id", equalTo(1))
.body("email", equalTo("alice@example.com"))
.body("role", equalTo("admin"));
}The schema validation checks the structure; the value assertions check the specific content. Both can fail independently.
Schemas for Array Responses
For endpoints that return lists:
src/test/resources/schemas/user-list-response.json:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "User List Response",
"type": "array",
"items": {
"$ref": "#/definitions/User"
},
"definitions": {
"User": {
"type": "object",
"required": ["id", "email", "name"],
"properties": {
"id": { "type": "integer" },
"email": { "type": "string", "format": "email" },
"name": { "type": "string" },
"role": { "type": "string" }
}
}
}
}@Test
void listUsers_returnsArrayMatchingSchema() {
given()
.when()
.get("/api/users")
.then()
.statusCode(200)
.body(matchesJsonSchemaInClasspath("schemas/user-list-response.json"))
.body("size()", greaterThan(0));
}Paginated Response Schema
Most production APIs wrap list responses in a pagination envelope:
src/test/resources/schemas/paginated-users.json:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["data", "pagination"],
"properties": {
"data": {
"type": "array",
"items": {
"type": "object",
"required": ["id", "email", "name"],
"properties": {
"id": { "type": "integer" },
"email": { "type": "string" },
"name": { "type": "string" }
}
}
},
"pagination": {
"type": "object",
"required": ["page", "perPage", "total", "totalPages"],
"properties": {
"page": { "type": "integer", "minimum": 1 },
"perPage": { "type": "integer", "minimum": 1 },
"total": { "type": "integer", "minimum": 0 },
"totalPages": { "type": "integer", "minimum": 0 }
}
}
},
"additionalProperties": false
}@Test
void listUsers_paginatedResponse_matchesSchema() {
given()
.queryParam("page", 1)
.queryParam("per_page", 20)
.when()
.get("/api/users")
.then()
.statusCode(200)
.body(matchesJsonSchemaInClasspath("schemas/paginated-users.json"))
.body("pagination.page", equalTo(1))
.body("data.size()", lessThanOrEqualTo(20));
}Error Response Schema
Standardizing error responses is important for API consumers. Define a schema for your error envelope and validate it in negative test cases:
src/test/resources/schemas/error-response.json:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["error", "status", "timestamp"],
"properties": {
"error": {
"type": "string",
"minLength": 1
},
"status": {
"type": "integer",
"minimum": 400,
"maximum": 599
},
"message": {
"type": "string"
},
"path": {
"type": "string"
},
"timestamp": {
"type": "string",
"format": "date-time"
}
},
"additionalProperties": false
}@Test
void getUser_notFound_errorMatchesSchema() {
given()
.when()
.get("/api/users/999999")
.then()
.statusCode(404)
.body(matchesJsonSchemaInClasspath("schemas/error-response.json"))
.body("status", equalTo(404))
.body("error", notNullValue());
}
@Test
void createUser_invalidInput_errorMatchesSchema() {
given()
.contentType("application/json")
.body("{\"email\":\"not-an-email\"}")
.when()
.post("/api/users")
.then()
.statusCode(400)
.body(matchesJsonSchemaInClasspath("schemas/error-response.json"));
}This ensures your error responses are consistent across all endpoints — a common issue in APIs where different handlers return errors in different formats.
Using $ref for Shared Definitions
Large APIs have many endpoints that return similar objects. Use JSON Schema $ref to define shared types once:
src/test/resources/schemas/common.json:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"Address": {
"type": "object",
"required": ["street", "city", "country"],
"properties": {
"street": { "type": "string" },
"city": { "type": "string" },
"state": { "type": "string" },
"postalCode": { "type": "string" },
"country": { "type": "string", "minLength": 2, "maxLength": 2 }
}
},
"Money": {
"type": "object",
"required": ["amount", "currency"],
"properties": {
"amount": { "type": "number", "minimum": 0 },
"currency": { "type": "string", "pattern": "^[A-Z]{3}$" }
}
}
}
}Reference these in other schemas:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["id", "total", "shippingAddress"],
"properties": {
"id": { "type": "string" },
"total": { "$ref": "common.json#/definitions/Money" },
"shippingAddress": { "$ref": "common.json#/definitions/Address" }
}
}Configuring Validation Strictly vs. Permissively
By default, REST Assured's schema validation is permissive about additional properties. You can tighten this:
import io.restassured.module.jsv.JsonSchemaValidatorSettings;
import io.restassured.module.jsv.JsonSchemaValidator;
// Configure globally in @BeforeAll
@BeforeAll
static void configureSchemaValidation() {
JsonSchemaValidator.settings = JsonSchemaValidatorSettings.settings()
.with().jsonSchemaFactory(
io.github.classgraph.utils.ReflectionUtils.invokeMethod(
null, null, null, null
)
)
.and().with().checkedValidation(true);
}A simpler approach: put "additionalProperties": false in your schemas. That's the schema-level way to reject responses with unexpected fields, which is often what you want — if the API starts returning an extra field, your test catches it.
Generating Schemas from Existing Responses
Writing schemas from scratch for complex responses is tedious. You can bootstrap by capturing a real response and generating a schema from it:
@Test
void captureUserResponseForSchemaGeneration() {
String responseBody = given()
.when()
.get("/api/users/1")
.then()
.statusCode(200)
.extract()
.body()
.asString();
System.out.println(responseBody);
// Paste this output into jsonschema.net or similar tool to generate a schema
}Tools like jsonschema.net or the json-schema-generator library can infer a schema from a sample response. Treat the generated schema as a starting point — review it and add required fields, format constraints, and enum values manually.
Schema Versioning
As your API evolves, schemas change. Keep schemas in version control alongside your tests. When you intentionally change an API response shape:
- Update the schema to reflect the new shape
- Verify all existing tests still pass
- Commit schema + test changes in the same PR as the API change
This makes schema changes visible in code review, which catches accidental breaking changes before they reach production.
Where Schema Validation Fits in Your Testing Strategy
Schema validation in REST Assured is part of a broader API contract story. If you're serious about API contracts, look at consumer-driven contract testing with Pact — it's a more formal approach where API consumers define the contracts and the provider verifies them independently.
For teams that need API testing without writing Java at all, tools like HelpMeTest let you define expected response shapes in plain English. Usage-based pricing ($0.003/run, no base fee) makes it accessible for QA engineers who need to validate API contracts but don't have a Java background.
For Java teams, REST Assured's schema validation is the right tool: it's lightweight, integrates cleanly with your existing test suite, and catches the structural API regressions that value assertions miss.
Summary
JSON Schema validation with REST Assured adds a layer of structural verification that individual field assertions can't provide efficiently. Define schemas in src/test/resources/schemas/, validate with matchesJsonSchemaInClasspath(), and combine schema checks with value assertions for comprehensive coverage. Define schemas for both success and error responses, use $ref to share common types, and keep schemas in version control so API contract changes are visible and intentional.