REST Assured, JSON Schema, or Pact?
This article started with a simple question: what should I test with REST Assured, what is better left to JSON Schema, and when does Pact actually add value?
Instead of comparing them in theory, I’ll start with one REST Assured test and gradually refactor it. Let’s see what changes – and what each approach catches.
Starting with Everything in REST Assured
Consider a simple Customer API:
GET /customers/123The response contains an id, name, email, status, and createdAt.
Let’s start by testing everything with REST Assured:
given()
.when()
.get("/customers/123")
.then()
.statusCode(200)
.body("id", equalTo(123))
.body("name", notNullValue())
.body("email", notNullValue())
.body("email.length()", lessThanOrEqualTo(100))
.body("status", anyOf(
equalTo("ACTIVE"),
equalTo("INACTIVE")))
.body("createdAt", notNullValue());There is nothing wrong with this test. But it mixes two kinds of checks: the behavior of this particular request and the structure of a valid Customer response. Let’s separate them.
Moving Structural Validation to JSON Schema
Some assertions describe the API response rather than the behavior of this particular scenario. We are not removing those checks; we are moving them into a JSON Schema.
given()
.when()
.get("/customers/123")
.then()
.statusCode(200)
.body("id", equalTo(123))
- .body("name", notNullValue())
- .body("email", notNullValue())
- .body("email.length()", lessThanOrEqualTo(100))
- .body("status", anyOf(
- equalTo("ACTIVE"),
- equalTo("INACTIVE")))
- .body("createdAt", notNullValue());We are not removing these checks. We are moving them into a JSON Schema:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": [
"id",
"name",
"email",
"status",
"createdAt"
],
"properties": {
"id": {
"type": "integer"
},
"name": {
"type": "string",
"maxLength": 100
},
"email": {
"type": "string",
"format": "email",
"maxLength": 100
},
"status": {
"type": "string",
"enum": ["ACTIVE", "INACTIVE"]
},
"createdAt": {
"type": "string",
"format": "date-time"
}
}
}Our REST Assured test now becomes:
given()
.when()
.get("/customers/123")
.then()
.statusCode(200)
.body("id", equalTo(123))
.body(matchesJsonSchemaInClasspath(
"customer-schema.json"
));We haven’t reduced coverage – we’ve moved structural validation into the schema. Pactflow makes a useful distinction here: a schema can describe valid API messages, but that is not necessarily the same thing as describing what an individual consumer relies on.
Adding Pact: Valid Does Not Mean Compatible
Get Popova Tatiana O’s stories in your inbox
Join Medium for free to get updates from this writer.
Our JSON Schema allows email to contain up to 100 characters. Suppose notification-service can handle only 20 characters. The API therefore permits something that one of its consumers cannot handle.
The Pact documentation describes contracts around the interactions consumers actually use, while the Pact FAQ explains why an API schema is not a replacement for consumer expectations.
This separation is also reflected in Contract Testing in Action by Marie Cruz and Lewis Prescott, which positions contract testing alongside other testing techniques rather than as their replacement. The book emphasizes that the real value comes from the complete consumer – provider flow: defining the contract on the consumer side and verifying it against the provider.
@Pact(consumer = "notification-service")
public V4Pact customerPact(PactDslWithProvider builder) {
return builder
.given("customer 123 exists")
.uponReceiving("a request for customer 123")
.path("/customers/123")
.method("GET")
.willRespondWith()
.status(200)
.body(new PactDslJsonBody()
.integerType("id", 123)
.stringMatcher(
"email",
"^.{1,20}$",
"anna@example.com"
))
.toPact(V4Pact.class);
}The consumer test generates the Pact contract.
Pact Contract
{
"consumer": {
"name": "notification-service"
},
"provider": {
"name": "customer-service"
},
"interactions": [
{
"description": "a request for customer 123",
"request": {
"method": "GET",
"path": "/customers/123"
},
"response": {
"status": 200,
"body": {
"id": 123,
"email": "anna@example.com"
},
"matchingRules": {
"body": {
"$.id": {
"matchers": [
{
"match": "integer"
}
]
},
"$.email": {
"matchers": [
{
"match": "regex",
"regex": "^.{1,20}$"
}
]
}
}
}
}
}
]
}The difference is now visible directly in the artifacts: JSON Schema allows email <= 100 characters, while the Pact contract expresses the notification-service limit of <= 20 characters.
Practical Java Pact example could be found in Baeldung.
Provider Verification
@Provider("customer-service")
@PactBroker
class CustomerProviderPactTest {
@BeforeEach
void setUp(PactVerificationContext context) {
context.setTarget(
new HttpTestTarget("localhost", 8080)
);
}
@TestTemplate
@ExtendWith(
PactVerificationInvocationContextProvider.class
)
void verifyPact(PactVerificationContext context) {
context.verifyInteraction();
}
}Suppose the provider returns
anna.mueller.long.address@example.com.
It is valid according to our JSON Schema but violates the expectation of notification-service.
Check …………………………………….. Result
REST Assured……………………………. PASS
JSON Schema (maxLength: 100) ……. PASS
Pact (consumer limit: 20) …………….. FAIL
And this is the distinction I was looking for. REST Assured checks the API interaction. JSON Schema checks the API structure and constraints. Pact checks whether the interaction is still compatible with what a particular consumer expects.
The API can work, the response can be valid, and the integration can still break.






