Using Swagger Request Validator to Validate Spring Cloud Contracts
3 CommentsLast Updated on June 10, 2019 by Simanta
The Swagger Request Validator is a slick project supported by the folks at Atlassian.
Using your API definition in Swagger v2 or OpenAPI v3, it allows you to programmatically validate your API matches your API specification.
The Swagger Request Validator may be used standalone, or with Spring MVC, Spring MockMVC, Spring Web Client, REST Assured, WireMock, or Pact.
Validations the Swagger Request Validator can perform include:
- Valid API Path / Operation
- Request Body – expected and if matches JSON Schema
- Missing Header Parameters
- Missing or Invalid query parameters
- Valid Content Type Headers
- Valid Accept Headers
- Response Body – if expected and if it matches the JSON schema
In this post, I’ll show you how to define contracts in OpenAPI for Spring Cloud Contract, and how to configure the Atlassian Swagger Request Validator to validate API interactions performed by tests automatically generated by Spring Cloud Contract.
In this example, we will look at a simple Payor service, to look up Payor details for a given Payor Id.
API Request
OpenAPI Operation
The OpenAPI operation is defined as follows:
paths:
/v1/payors/{payorId}:
get:
summary: Get Payor Details by ID
description: |
This API operation provide you the account balance of the given payor id.
operationId: getPayor
tags:
- Payor Service
parameters:
- name: payorId
in: path
description: The account owner Payor ID
required: true
schema:
type: string
format: uuidThis is a simple API request. An HTTP GET is performed against the path /v1/payors/{payorId}; where payorId is the UUID of the desired payor.
Spring Framework Rest Controller
The Spring Framework Rest Controller is implemented as follows:
@RestController
@RequestMapping("/v1/payors")
public class PayorControllor {
private final PayorService payorService;
public PayorControllor(PayorService payorService) {
this.payorService = payorService;
}
@GetMapping(value = "/{payorId}")
public ResponseEntity<Payor> getPayor(@PathVariable("payorId") UUID payorId){
Optional<Payor> optionalPayor = payorService.getPayor(payorId);
if (optionalPayor.isPresent()) {
return ResponseEntity.of(optionalPayor);
} else {
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}
}
}API Response
Open API Response Schema
The response for the operation is defined in YAML as follows:
responses:
'200':
description: Get Payor Details
content:
application/json:
schema:
type: object
required:
- payorId
- payorName
- address
- primaryContactName
- primaryContactPhone
- primaryContactEmail
- language
properties:
payorId:
type: string
format: uuid
payorName:
type: string
address:
type: object
required:
- line1
- city
- zipOrPostcode
- country
properties:
line1:
type: string
minLength: 2
maxLength: 255
line2:
type: string
minLength: 0
maxLength: 255
line3:
type: string
minLength: 0
maxLength: 255
line4:
type: string
minLength: 0
maxLength: 255
city:
type: string
minLength: 2
maxLength: 100
countyOrProvince:
type: string
minLength: 2
maxLength: 100
zipOrPostcode:
type: string
minLength: 2
maxLength: 30
country:
type: string
minLength: 2
maxLength: 50
primaryContactName:
type: string
description: Name of primary contact for the payor.
primaryContactPhone:
type: string
description: Primary contact phone number for the payor.
primaryContactEmail:
type: string
format: email
description: Primary contact email for the payor.
fundingAccountRoutingNumber:
type: string
description: The funding account routing number to be used for the payor.
fundingAccountAccountNumber:
type: string
description: The funding account number to be used for the payor.
fundingAccountAccountName:
type: string
description: The funding account name to be used for the payor.
kycState:
type: string
description: The kyc state of the payor.
enum: [FAILED_KYC, PASSED_KYC, REQUIRES_KYC]
manualLockout:
type: boolean
description: Whether or not the payor has been manually locked by the backoffice.
payeeGracePeriodProcessingEnabled:
type: boolean
description: Whether grace period processing is enabled.
payeeGracePeriodDays:
type: integer
description: The grace period for paying payees in days.
collectiveAlias:
type: string
description: How the payor has chosen to refer to payees.
supportContact:
type: string
description: The payor’s support contact address.
dbaName:
type: string
description: The payor’s 'Doing Business As' name.
allowsLanguageChoice:
type: boolean
description: Whether or not the payor allows language choice in the UI.
reminderEmailsOptOut:
type: boolean
description: Whether or not the payor has opted-out of reminder emails being sent.
language:
type: string
description: The payor’s language preference. Must be one of [EN, FR].
enum: [EN, FR]
includesReports:
type: booleanJava Response Objects
The following Java Objects are defined to support the response:
Payor.java
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Payor {
/**
* The payor id (UUID).
*/
@NotNull(message="The Payor Id is mandatory")
private String payorId;
/**
* The name of the payor.
*/
@NotNull(message="The Payor Name is mandatory")
private String payorName;
/**
* The address of the payor.
*/
@NotNull(message="The Address is mandatory")
private Address address;
/**
* Name of primary contact for the payor.
*/
@NotNull(message="The Primary Contact Name is mandatory")
private String primaryContactName;
/**
* Primary contact phone number for the payor.
*/
@NotNull(message="The Primary Contact Phone is mandatory")
private String primaryContactPhone;
/**
* Primary contact email for the payor.
*/
@NotNull(message="The Primary Contact Email is mandatory")
private String primaryContactEmail;
/**
* The funding account routing number to be used for the payor.
*/
private String fundingAccountRoutingNumber;
/**
* The funding account number to be used for the payor.
*/
private String fundingAccountAccountNumber;
/**
* The funding account name to be used for the payor.
*/
private String fundingAccountAccountName;
/**
* The kyc state of the payor.
*/
private KycState kycState;
/**
* Whether or not the payor has been manually locked by the backoffice.
*/
private Boolean manualLockout;
/**
* Whether grace period processing is enabled.
*/
private Boolean payeeGracePeriodProcessingEnabled;
/**
* The grace period for paying payees in days.
*/
private Integer payeeGracePeriodDays;
/**
* How the payor has chosen to refer to payees
*/
private String collectiveAlias;
/**
* The payor's support contact address
*/
private String supportContact;
/**
* The payor's 'Doing Business As' name
*/
private String dbaName;
/**
* Whether or not the payor allows language choice in the UI
*/
private Boolean allowsLanguageChoice;
/**
* Whether or not the payor has opted-out of reminder emails being sent
*/
private Boolean reminderEmailsOptOut;
/**
* The payor's language preference
*/
@NotNull(message = "The Preferred Language is mandatory")
private Language language = Language.EN;
// flag to indicate whether a payor should see reports
private Boolean includesReports;
}
Address.java
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Address {
@Size(min=2, max=255, message = "Line 1 of the address must be between {min} and {max} characters long")
@NotNull(message = "Line 1 of the address is mandatory")
private String line1;
@Size(max = 255, message = "Line 2 of the address can be no more than {max} characters long")
private String line2;
@Size(max = 255, message = "Line 3 of the address can be no more than {max} characters long")
private String line3;
@Size(max = 255, message = "Line 4 of the address can be no more than {max} characters long")
private String line4;
@Size(min=2, max=100, message = "City must be between {min} and {max} characters long")
@NotNull(message = "City is mandatory")
private String city;
@Size(min=2, max=100, message = "County or Province must be between {min} and {max} characters long")
private String countyOrProvince;
@Size(min=2, max=30, message = "Zip or Postcode must be between {min} and {max} characters long")
private String zipOrPostcode;
@Size(min=2, max=50, message = "Country must be between {min} and {max} characters long")
@NotNull(message = "Country is mandatory")
private String country;
}Language.java
public enum Language {
EN, FR
}KycState.java
public enum KycState {
FAILED_KYC, PASSED_KYC, REQUIRES_KYC;
}
Spring Cloud Contracts Defined in OA3
In this example, we will define 3 contracts. This example uses my Spring Cloud Contact OA3 converter explained here.
paths:
/v1/payors/{payorId}:
get:
....
x-contracts:
- contractId: 1
name: Test Good Response
contractPath: "/v1/payors/0a818933-087d-47f2-ad83-2f986ed087eb"
- contractId: 2
name: Test Not Found
contractPath: "/v1/payors/00000000-0000-0000-0000-000000000000"
- contractId: 3
name: Test Bad Schema
contractPath: "/v1/payors/16ea5c75-1476-4651-b117-bcb33d77f2b5"
....
responses:
'200':
x-contracts:
- contractId: 1
body:
payorId: "0a818933-087d-47f2-ad83-2f986ed087eb"
'404':
description: Payor Id Not Found
x-contracts:
- contractId: 2
'500':
description: Server Error
x-contracts:
- contractId: 3
body:
payorId: "0a818933-087d-47f2-ad83-2f986ed087eb"Maven POM
In your Maven POM, you will need to add dependencies for Spring Cloud Contract, the SFG Spring Cloud Contract OA3 Converter, Spring (in general), Atlassian.
Note the configuration of the Spring Cloud Contract Maven plugin.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>guru.sfgpay</groupId>
<artifactId>sfg-payor-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>sfg-payor-service</name>
<description>Payor Service</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.M3</spring-cloud.version>
<swagger-request-validator.version>2.1.0</swagger-request-validator.version>
<spring-cloud-dependencies.version>Finchley.BUILD-SNAPSHOT</spring-cloud-dependencies.version>
<spring-cloud-contract.version>2.0.2.RELEASE</spring-cloud-contract.version>
<scc-oa3-version>2.0.2.2</scc-oa3-version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-verifier</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>guru.springframework</groupId>
<artifactId>spring-cloud-contract-oa3</artifactId>
<version>${scc-oa3-version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.atlassian.oai</groupId>
<artifactId>swagger-request-validator-core</artifactId>
<version>${swagger-request-validator.version}</version>
<!--<scope>test</scope>-->
</dependency>
<dependency>
<groupId>com.atlassian.oai</groupId>
<artifactId>swagger-request-validator-mockmvc</artifactId>
<version>${swagger-request-validator.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.atlassian.oai</groupId>
<artifactId>swagger-request-validator-springmvc</artifactId>
<version>${swagger-request-validator.version}</version>
</dependency>
<dependency>
<groupId>com.atlassian.oai</groupId>
<artifactId>swagger-request-validator-restassured</artifactId>
<version>${swagger-request-validator.version}</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>${spring-cloud-contract.version}</version>
<extensions>true</extensions>
<configuration>
<testMode>EXPLICIT</testMode>
<packageWithBaseClasses>guru.sfgpay</packageWithBaseClasses>
<contractsPath>classpath:/oa3</contractsPath>
<contractsDirectory>${pom.basedir}/src/main/resources/oa3</contractsDirectory>
</configuration>
<dependencies>
<!--needed to include oa3 converter-->
<dependency>
<groupId>guru.springframework</groupId>
<artifactId>spring-cloud-contract-oa3</artifactId>
<version>${scc-oa3-version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
</project>
Enabling OA3 Validation With Swagger Request Validator
There are several options to enable OA3 validation with the Swagger Request Validator.
A challenge to deal with is how Spring Cloud Contract (SCC) generates its unit tests.
SCC by default uses Rest Assured, which is supported by the Swagger Request Validator.
But working from the SCC DSL, I was unable to figure out how to enable the OA3 validation.
The easiest approach I was able to find was avoid the Mock Servlet requests and run integration tests using embedded Tomcat (via Spring Boot).
With this integration, its easy to configure an interceptor for the OA3 validation.
OpenAPI Validation Interceptor
Here is the configuration for the OpenAPI Validation Interceptor
@Configuration
@Profile("oa3-validate")
public class OpenApiValidationConfig implements WebMvcConfigurer {
private final OpenApiValidationInterceptor validationInterceptor;
@Autowired
public OpenApiValidationConfig() throws IOException {
this.validationInterceptor = new OpenApiValidationInterceptor(OpenApiInteractionValidator
.createFor("/oa3/openapi.yml").build());
}
@Bean
public Filter validationFilter() {
return new OpenApiValidationFilter(
true, // enable request validation
true // enable response validation
);
}
@Override
public void addInterceptors(final InterceptorRegistry registry) {
registry.addInterceptor(validationInterceptor);
}
}Spring Cloud Contract Configuration
Spring Cloud Contract is configured to use Explicit mode – ie web server on localhost.
Spring Cloud Contract Base Test Class
@RunWith(SpringRunner.class)
@SpringBootTest(classes = SfgPayorServiceApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("oa3-validate")
public class ContractVerifierBase {
//by classpath
private static final String OA3_URL = "/oa3/openapi.yml";
private final OpenApiValidationFilter validationFilter = new OpenApiValidationFilter(OA3_URL);
@LocalServerPort
int port;
@Before
public void setUp() throws Exception {
RestAssured.baseURI = "http://localhost";
RestAssured.port = this.port;
}
}Spring Cloud Contract Maven Configuration
Note the following configuration properties for SCC. Very important to set testMode to EXPLICIT.
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>${spring-cloud-contract.version}</version>
<extensions>true</extensions>
<configuration>
<testMode>EXPLICIT</testMode>
<packageWithBaseClasses>guru.sfgpay</packageWithBaseClasses>
<contractsPath>classpath:/oa3</contractsPath>
<contractsDirectory>${pom.basedir}/src/main/resources/oa3</contractsDirectory>
</configuration>
<dependencies>
<!--needed to include oa3 converter-->
<dependency>
<groupId>guru.springframework</groupId>
<artifactId>spring-cloud-contract-oa3</artifactId>
<version>${scc-oa3-version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>Test Generated by Spring Cloud Contract
From the configuration above, the following tests are generated by Spring Cloud Contact.
public class ContractVerifierTest extends ContractVerifierBase {
public ContractVerifierTest() {
}
@Test
public void validate_test_Good_Response() throws Exception {
RequestSpecification request = RestAssured.given();
Response response = (Response)RestAssured.given().spec(request).get("/v1/payors/0a818933-087d-47f2-ad83-2f986ed087eb", new Object[0]);
SpringCloudContractAssertions.assertThat(response.statusCode()).isEqualTo(200);
DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
JsonAssertion.assertThatJson(parsedJson).field("['payorId']").isEqualTo("0a818933-087d-47f2-ad83-2f986ed087eb");
}
@Test
public void validate_test_Not_Found() throws Exception {
RequestSpecification request = RestAssured.given();
Response response = (Response)RestAssured.given().spec(request).get("/v1/payors/00000000-0000-0000-0000-000000000000", new Object[0]);
SpringCloudContractAssertions.assertThat(response.statusCode()).isEqualTo(404);
}
@Test
public void validate_test_Bad_Schema() throws Exception {
RequestSpecification request = RestAssured.given();
Response response = (Response)RestAssured.given().spec(request).get("/v1/payors/16ea5c75-1476-4651-b117-bcb33d77f2b5", new Object[0]);
SpringCloudContractAssertions.assertThat(response.statusCode()).isEqualTo(500);
DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
JsonAssertion.assertThatJson(parsedJson).field("['payorId']").isEqualTo("0a818933-087d-47f2-ad83-2f986ed087eb");
}
}Test Output
For the 3rd test, I configured the service to return back a Payor object missing required properties.
Here is the console output of the test where you can see the validation failure.
2018-12-13 15:36:17.653 ERROR 27406 --- [o-auto-1-exec-2] c.a.o.v.s.DefaultValidationReportHandler : OpenAPI location=RESPONSE key=GET#/v1/payors/16ea5c75-1476-4651-b117-bcb33d77f2b5 levels=ERROR messages=Validation failed. [ERROR][RESPONSE][GET /v1/payors/16ea5c75-1476-4651-b117-bcb33d77f2b5] Object has missing required properties (["language","payorId","payorName","primaryContactEmail","primaryContactName","primaryContactPhone"]) [ERROR][RESPONSE][GET /v1/payors/16ea5c75-1476-4651-b117-bcb33d77f2b5] [Path '/address'] Object has missing required properties (["city","country","line1","zipOrPostcode"])
Source Code
The complete source code is available here in my GitHub repository.
Conclusion
With the above configuration, your OpenAPI specification will be validated with every request / response made my Spring Cloud Contract.

Ahmed
Thanks a lot for your help.
Pandurang
I don’t find test class ContractVerifierTest in the git repo https://github.com/springframeworkguru/sfg-payor-service that you have shared.
Gaétan
for:
primaryContactEmail:
type: string
format: email
description: Primary contact email for the payor.
Why does this “format: email” does not generate an @Email annotation on java side?
If it’s normal, then the format is useless here, right?