Skip to content

ZenWave SDK API-First tooling for OpenAPI

Whether you write your OpenAPI definition manually or generate it using ZenWave Domain Model as an Interface Definition Language (IDL), ZenWave SDK provides several tools to build different aspects of your application:

  • Use OpenAPI Generator Maven plugin to generate SpringMVC Controller Interfaces and DTOs from OpenAPI.
  • Use OpenAPIControllersPlugin to generate SpringMVC Controllers (skeletons) implementing OpenAPI Controller Interfaces.
  • Use SpringWebTestClientPlugin to generate Spring WebTestClient Tests for your SpringMVC Controllers.
  • Use OpenAPIKaratePlugin to generate KarateDSL API Tests for your REST APIs from your OpenAPI specifications.

You can also:

The following examples are based on Customer Service JPA Example Project and its OpenAPI definition:

OpenAPI definition for Customer Service JPA Example

Navigate to source

This is how you can configure openapi-generator-maven-plugin in your pom.xml to make it compatible with OpenAPIControllersPlugin:

openapi-generator-maven-plugin in pom.xml

Navigate to source

Important configuration properties for compatibility with OpenAPIControllersPlugin:

  • <useSpringBoot3>true</useSpringBoot3> use SpringBoot 3 and jakarta annotations
  • <openApiNullable>false</openApiNullable> we use java.util.Optional instead
  • <useOptional>false</useOptional> do not use java.util.Optional for parameters
  • <useTags>true</useTags> required for grouping the operations in services by tag
  • <sortParamsByRequiredFlag>false</sortParamsByRequiredFlag> required for matching path params in generated code

Generating SpringMVC Controller Interfaces and DTOs from OpenAPI yaml

Section titled “Generating SpringMVC Controller Interfaces and DTOs from OpenAPI yaml”

Previous openapi-generator-maven-plugin will generate as part of the maven build the following java code:

  • target/generated-sources/openapi/src/main/java/io/zenwave360/example/adapters/web/CustomerApi.java
  • target/generated-sources/openapi/src/main/java/io/zenwave360/example/adapters/web/model/CustomerDTO.java
  • target/generated-sources/openapi/src/main/java/io/zenwave360/example/adapters/web/model/AddressDTO.java
  • target/generated-sources/openapi/src/main/java/io/zenwave360/example/adapters/web/model/PaymentMethodDTO.java
  • target/generated-sources/openapi/src/main/java/io/zenwave360/example/adapters/web/model/PaymentMethodTypeDTO.java
  • target/generated-sources/openapi/src/main/java/io/zenwave360/example/adapters/web/model/CustomerSearchCriteriaDTO.java
  • target/generated-sources/openapi/src/main/java/io/zenwave360/example/adapters/web/model/CustomerPaginatedDTO.java
  • target/generated-sources/openapi/src/main/java/io/zenwave360/example/adapters/web/model/PageDTO.java

These classes are part of your project compilee sources and will be compiled and packaged as part of your application. In the next section we will be implementing CustomerApi.java generated interface.

Do not edit this code by hand, it will be overwritten by the maven build. You will need to edit openapi.yml definition instead and then run maven build again, for instance:

Terminal window
mvn clean test-compile

Generating SpringMVC Controller Implementations from OpenAPI yaml

Section titled “Generating SpringMVC Controller Implementations from OpenAPI yaml”

While openapi-generator-maven-plugin can generate SpringMVC annotated Controller Interfaces and their DTOs, you will need to implement the interface methods yourself.

Thankfully, ZenWave SDK provides a plugin to generate SpringMVC Controller Implementations from OpenAPI yaml.

With OpenAPIControllersPlugin you can generate SpringMVC Controller Implementations from OpenAPI yaml.

When using the `OpenAPIControllersPlugin`, the following

this zdl script:
OpenAPIControllersPlugin {
openapiFile "src/main/resources/public/apis/openapi.yml"
}

Navigate to source

would produce this:

CustomerApiController.java
@RestController
@RequestMapping("/api")
public class CustomerApiController implements CustomerApi {
private final Logger log = LoggerFactory.getLogger(getClass());
@Autowired
private NativeWebRequest request;
private CustomerService customerService;
@Autowired
public CustomerApiController setCustomerService(CustomerService customerService) {
this.customerService = customerService;
return this;
}
private CustomerDTOsMapper mapper = CustomerDTOsMapper.INSTANCE;
public CustomerApiController(CustomerService customerService) {
this.customerService = customerService;
}
public Optional<NativeWebRequest> getRequest() {
return Optional.ofNullable(request);
}
@Override
public ResponseEntity<CustomerDTO> createCustomer(CustomerDTO reqBody) {
log.debug("REST request to createCustomer: {}", reqBody);
var input = mapper.asCustomer(reqBody);
var customer = customerService.createCustomer(input);
CustomerDTO responseDTO = mapper.asCustomerDTO(customer);
return ResponseEntity.status(201).body(responseDTO);
}
@Override
public ResponseEntity<CustomerDTO> getCustomer(Long id) {
log.debug("REST request to getCustomer: {}", id);
var customer = customerService.getCustomer(id);
if (customer.isPresent()) {
CustomerDTO responseDTO = mapper.asCustomerDTO(customer.get());
return ResponseEntity.status(200).body(responseDTO);
} else {
return ResponseEntity.notFound().build();
}
}
@Override
public ResponseEntity<CustomerDTO> updateCustomer(Long id, CustomerDTO reqBody) {
log.debug("REST request to updateCustomer: {}, {}", id, reqBody);
var input = mapper.asCustomer(reqBody);
var customer = customerService.updateCustomer(id, input);
if (customer.isPresent()) {
CustomerDTO responseDTO = mapper.asCustomerDTO(customer.get());
return ResponseEntity.status(200).body(responseDTO);
} else {
return ResponseEntity.notFound().build();
}
}
@Override
public ResponseEntity<Void> deleteCustomer(Long id) {
log.debug("REST request to deleteCustomer: {}", id);
customerService.deleteCustomer(id);
return ResponseEntity.status(204).build();
}
@Override
public ResponseEntity<CustomerPaginatedDTO> searchCustomers(
Integer page, Integer limit, List<String> sort, CustomerSearchCriteriaDTO reqBody) {
log.debug("REST request to searchCustomers: {}, {}, {}, {}", page, limit, sort, reqBody);
var input = mapper.asCustomerSearchCriteria(reqBody);
var customerPage = customerService.searchCustomers(input, pageOf(page, limit, sort));
var responseDTO = mapper.asCustomerPaginatedDTO(customerPage);
return ResponseEntity.status(200).body(responseDTO);
}
protected Pageable pageOf(Integer page, Integer limit, List<String> sort) {
Sort sortOrder = sort != null
? Sort.by(sort.stream()
.map(sortParam -> {
String[] parts = sortParam.split(":");
String property = parts[0];
Sort.Direction direction =
parts.length > 1 ? Sort.Direction.fromString(parts[1]) : Sort.Direction.ASC;
return new Sort.Order(direction, property);
})
.toList())
: Sort.unsorted();
return PageRequest.of(page != null ? page : 0, limit != null ? limit : 10, sortOrder);
}
}

Navigate to source

And Unit Tests for your Controller:

CustomerApiControllerTest.java (excerpt)

Navigate to source

You can also generate API Tests for your REST APIs from your OpenAPI specifications.

Generating Spring WebTestClient Tests from OpenAPI yaml

Section titled “Generating Spring WebTestClient Tests from OpenAPI yaml”

WithWebTestClient you can test your REST API via HTTP semantics while in the same JVM process as your application.

ZenWaveSpringWebTestClientPlugin can generate both single endpoint tests as well as business flows spanning multiple endpoints:

When using the `SpringWebTestClientPlugin`, the following

zenwave-scripts.zw
SpringWebTestClientPlugin {
openapiFile "src/main/resources/public/apis/openapi.yml"
}

Navigate to source

would produce this API test:

CustomerApiIntegrationTest.java (excerpt)
@Test
void testCreateCustomer_201() {
var requestBody =
"""
{
"email": "jane.doe@example.com",
"name": "Jane Doe",
"addresses": [
{
"city": "Othertown",
"street": "456 Elm St"
}
],
"paymentMethods": [
{
"type": "VISA",
"cardNumber": "6543210987654321"
}
]
}
""";
webTestClient
.method(POST)
.uri("/api/customers")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(requestBody)
.exchange()
.expectStatus()
.isEqualTo(201)
.expectHeader()
.contentType(MediaType.APPLICATION_JSON)
.expectBody()
.jsonPath("$.id")
.isNotEmpty()
.jsonPath("$.version")
.isNotEmpty()
.jsonPath("$.name")
.isNotEmpty()
.jsonPath("$.email")
.isNotEmpty()
.jsonPath("$.addresses")
.isNotEmpty()
.jsonPath("$.addresses")
.isArray()
.jsonPath("$.addresses[0].street")
.isNotEmpty()
.jsonPath("$.addresses[0].city")
.isNotEmpty()
.jsonPath("$.paymentMethods")
.isNotEmpty()
.jsonPath("$.paymentMethods")
.isArray()
.jsonPath("$.paymentMethods[0].id")
.isNotEmpty()
.jsonPath("$.paymentMethods[0].version")
.isNotEmpty()
.jsonPath("$.paymentMethods[0].type")
.isNotEmpty()
.jsonPath("$.paymentMethods[0].cardNumber")
.isNotEmpty();
}

Navigate to source

When using the `SpringWebTestClientPlugin`, the following

zenwave-scripts.zw
SpringWebTestClientPlugin {
openapiFile "src/main/resources/public/apis/openapi.yml"
groupBy businessFlow
businessFlowTestName CreateUpdateDeleteCustomerIntegrationTest
operationIds createCustomer,updateCustomer,deleteCustomer,getCustomer
}

Navigate to source

would produce this Business Flow API test:

CustomerApiIntegrationTest

Navigate to source

You can control whether these are Unit or Integration Tests with @SpringBootTest in

BaseWebTestClientTest.java

Navigate to source

As always, it’s your responsibility as the developer to provide test data and parameters that match the initial state, whether you’re using a containerized database or an in-memory implementation.

Generating KarateDSL Tests from OpenAPI yaml

Section titled “Generating KarateDSL Tests from OpenAPI yaml”

You can also generate KarateDSL Tests from OpenAPI yaml. Both single endpoint tests as well as business flows spanning multiple endpoints.

When using the `OpenAPIKaratePlugin`, the following

zenwave-scripts.zw
OpenAPIKaratePlugin {
openapiFile "src/main/resources/public/apis/openapi.yml"
}

Navigate to source

would produce this API test:

CustomerApi.feature KarateDSL API Test
@openapi-file=src/main/resources/public/apis/openapi.yml
Feature: CustomerApi
Background:
* url baseUrl
# * def auth = { username: '', password: '' }
* def authHeader = call read('classpath:karate-auth.js') auth
* configure headers = authHeader || {}
@operationId=createCustomer
Scenario: createCustomer
Given path '/customers'
And request
"""
{
"name" : "name-iv8wf61cpxe13fa3j0",
"email" : "kraig.hermann@gmail.com",
"addresses" : [ {
"street" : "street-wivpwm357mj4r1",
"city" : "Connellyside"
} ],
"paymentMethods" : [ {
"type" : "VISA",
"cardNumber" : "cardNumber-ydvoeke77t3z"
} ]
}
"""
When method post
Then status 201
* def createCustomerResponse = response
* def customerId = response.id
# TODO: Add response validation
@operationId=getCustomer
Scenario: getCustomer
* def pathParams = { id: 53 }
Given path '/customers/', pathParams.id
When method get
Then status 200
* def getCustomerResponse = response
* def customerId = response.id
# TODO: Add response validation
@operationId=updateCustomer
Scenario: updateCustomer
* def pathParams = { id: 53 }
Given path '/customers/', pathParams.id
And request
"""
{
"name" : "name-t58rqm1n6zb",
"email" : "reta.wiza@gmail.com",
"addresses" : [ {
"street" : "street-e2pn9ih8t7du14h93",
"city" : "East Genesis"
} ],
"paymentMethods" : [ {
"type" : "MASTERCARD",
"cardNumber" : "cardNumber-qx3ag249vxzjgkh"
} ]
}
"""
When method put
Then status 200
* def updateCustomerResponse = response
* def customerId = response.id
# TODO: Add response validation
@operationId=deleteCustomer
Scenario: deleteCustomer
* def pathParams = { id: 89 }
Given path '/customers/', pathParams.id
When method delete
Then status 204
* def deleteCustomerResponse = response
# TODO: Add response validation
@operationId=searchCustomers
Scenario: searchCustomers
Given path '/customers/search'
And def queryParams = { page: 71, limit: 20, sort: ['name:asc'] }
And request
"""
{
"name" : "name-xrx9m9s1",
"email" : "tyron.heidenreich@yahoo.co",
"city" : "Bellamouth",
"state" : "state-y1x18r2h"
}
"""
When method post
Then status 201
* def searchCustomersResponse = response
* def customerPaginatedId = response.id
# TODO: Add response validation

Navigate to source

When using the `OpenAPIKaratePlugin`, the following

zenwave-scripts.zw
OpenAPIKaratePlugin {
openapiFile "src/main/resources/public/apis/openapi.yml"
groupBy businessFlow
businessFlowTestName CreateUpdateDeleteCustomerKarateTest
operationIds createCustomer,updateCustomer,deleteCustomer,getCustomer
}

Navigate to source

would produce this Business Flow API test:

CreateUpdateDeleteCustomerKarateTest.feature KarateDSL Business Flow Test
@openapi-file=src/main/resources/public/apis/openapi.yml
Feature: CreateUpdateDeleteCustomerKarateTest
Background:
* url baseUrl
# * def auth = { username: '', password: '' }
* def authHeader = call read('classpath:karate-auth.js') auth
* configure headers = authHeader || {}
@business-flow
@operationId=createCustomer,updateCustomer,deleteCustomer,getCustomer
Scenario: CreateUpdateDeleteCustomerKarateTest
# createCustomer
Given path '/customers'
And request
"""
{
"id" : 21,
"version" : 13,
"name" : "name-5ugq20772n1",
"email" : "rosaria.hyatt@gmail.com",
"addresses" : [ {
"street" : "street-z",
"city" : "West Edmundbury"
} ],
"paymentMethods" : [ {
"id" : 37,
"version" : 10,
"type" : "MASTERCARD",
"cardNumber" : "cardNumber-9c2xzm77ig5"
} ]
}
"""
When method post
Then status 201
* def createCustomerResponse = response
* def customerId = response.id
# TODO: Add response validation
# updateCustomer
* def pathParams = { id: 2 }
Given path '/customers/', pathParams.id
And request
"""
{
"id" : 79,
"version" : 7,
"name" : "name-mu8j1eqcaoy2a7ex9uup1",
"email" : "jerica.emmerich@hotmail.co",
"addresses" : [ {
"street" : "street-7ug0jz4c5ghumuz2yst",
"city" : "Sydneychester"
} ],
"paymentMethods" : [ {
"id" : 30,
"version" : 82,
"type" : "MASTERCARD",
"cardNumber" : "cardNumber-f"
} ]
}
"""
When method put
Then status 200
* def updateCustomerResponse = response
* def customerId = response.id
# TODO: Add response validation
# deleteCustomer
* def pathParams = { id: 42 }
Given path '/customers/', pathParams.id
When method delete
Then status 204
* def deleteCustomerResponse = response
# TODO: Add response validation
# getCustomer
* def pathParams = { id: 71 }
Given path '/customers/', pathParams.id
When method get
Then status 404
* def getCustomerResponse = response
* def customerId = response.id
# TODO: Add response validation

Navigate to source