OpenAPI Generator Custom Templates
1. Introduction
Open API is a specification for designing and documenting RESTful APIs.OpenAPI generator is a tool used in API-first development as it can generate client and server source code from OpenAPI 2.0/3.x documents. It supports multiple languages and frameworks. Although most of the time the generated code is ready to be used without modification, there are scenarios in which we need to customize it. In this tutorial, I will demonstrate how to use spring boot openapi generator custom templates in the following steps:
- Create a maven project and configure “
openapi-generator-maven-plugin
“. - Create an OpenAPI specification –
products.yaml
. - Execute the maven
generate-source
command to generate source code from theproducts.yaml
file. - Create an implementation class for the generated interface.
- Create Junit tests.
- Update the
products.yaml
file with the “x-spring-cacheable
” vendor-specific extension with the default setting. - Update the
products.yaml
file with the “x-spring-cacheable
” vendor-specific extension and set the cache name. - Re-generate the source code with the built-in template and test with Junit tests.
- Re-generate the source code with a custom template and test with Junit tests.
2. Setup Maven Project for OpenAPI Generator
OpenAPI generator supportsa wide variety of generators. In this example, I will add “openapi-generator-maven-plugin
” in thepom.xml
file and generate code based on theproducts.yaml
file via the “spring
” generator.
pom.xml
<?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><artifactId>spring-boot-openapi</artifactId><name>spring-boot-openapi</name><packaging>jar</packaging><description>OpenAPI Generator module</description><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.2.5</version><relativePath /></parent><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency><dependency><groupId>jakarta.validation</groupId><artifactId>jakarta.validation-api</artifactId></dependency><dependency><groupId>javax.annotation</groupId><artifactId>javax.annotation-api</artifactId><version>1.3.2</version></dependency><dependency><groupId>io.swagger.core.v3</groupId><artifactId>swagger-annotations</artifactId><version>2.2.20</version></dependency></dependencies><build><plugins><plugin><groupId>org.openapitools</groupId><artifactId>openapi-generator-maven-plugin</artifactId><version>7.5.0</version><executions><execution><goals><goal>generate</goal></goals><configuration><inputSpec>${project.basedir}/src/main/resources/api/products.yaml</inputSpec><generatorName>spring</generatorName><supportingFilesToGenerate>ApiUtil.java</supportingFilesToGenerate><!--<templateResourcePath>${project.basedir}/src/main/resources/templates/JavaSpring</templateResourcePath>--><globalProperties><debugOpenAPI>true</debugOpenAPI></globalProperties><configOptions><delegatePattern>true</delegatePattern><apiPackage>com.zheng.demo.openapi.products.api</apiPackage><modelPackage>com.zheng.demo.openapi.products.api.model</modelPackage><documentationProvider>source</documentationProvider><dateLibrary>java8</dateLibrary><openApiNullable>false</openApiNullable><useJakartaEe>true</useJakartaEe><useSpringBoot3>true</useSpringBoot3></configOptions></configuration></execution></executions></plugin><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build><properties><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target></properties></project>
- line 54: add the
openapi-generator-maven-plugin
plugin. - line 63: config the
inputSpec
with the products.yaml. - line 64, use the “
spring
” generator. - line 73: set
delegatePattern
totrue
. - line 74: name
apiPackage
tocom.zheng.demo.openapi.products.api
. - line 75: name
modelPackage
tocom.zheng.demo.openapi.products.api.model
. - line 80: set
useJakartaEe
totrue
to use the Jakarta validation. - line 81: set
useSpringBoot3
totrue
.
Launchswagger editor at any browser and create a simple RestFul API specification to create and get a product. Save the specification in theproducts.yaml
file.
Here is the screenshot of products API.
Here is the product API’s YAML specification.
products.yaml
openapi: 3.0.0info: title: Product API version: 1.0.0servers: - description: Test server url: http://localhost:8080paths: /products/{id}: get: tags: - products summary: Get product detail for a given product id operationId: getProduct security: - ApiKey: - Product.Read parameters: - name: id in: path required: true description: Product's identifier schema: type: number responses: '200': description: OK content: application/json: schema: $ref: '#/components/schemas/ProductDO' /products: post: tags: - products summary: Create a product operationId: createProduct requestBody: description: Create a new prodcut in the store content: application/json: schema: $ref: '#/components/schemas/ProductDO' required: true security: - ApiKey: - Product.Create responses: '200': description: OK content: application/json: schema: $ref: '#/components/schemas/ProductDO'components: securitySchemes: ApiKey: type: apiKey in: header name: X-API-KEY schemas: ProductDO: description: Product Information type: object properties: id: type: number description: product id name: type: string description: product name price: type: number description: product price value
Copy theproducts.yaml
file and create two versions so there are 3 files underresources\api folder
:
api folder
C:\MaryTools\workspace\JCG\spring-boot-openapi\src\main\resources\api>dir Volume in drive C is OS Volume Serial Number is 92BA-6AB7 Directory of C:\MaryTools\workspace\JCG\spring-boot-openapi\src\main\resources\api05/04/2024 07:56 PM .05/03/2024 09:13 PM ..05/04/2024 07:55 PM 1,734 products.yaml05/04/2024 07:40 PM 1,765 products1.yaml05/04/2024 08:59 AM 1,786 products2.yaml 3 File(s) 5,285 bytes 2 Dir(s) 109,910,233,088 bytes freeC:\MaryTools\workspace\JCG\spring-boot-openapi\src\main\resources\api>
- The
products.yaml
has no vendor-specific extension, therefore can be generated without any customization. - The
products1.yaml
file has a vendor-spec extension and sets thex-spring-cacheable
astrue
. - The
products2.yaml
file sets thex-spring-cacheable
with the defined cache name.
The difference betweenproducts1.yaml
andproducts2.yaml
is showing in the following screenshot:
3. API Implementation
In this step, I will generate a spring boot server stub with the “spring
” generator via its built-in template and create the implementation class and test the generated code with Junit tests.
3.1 Create Spring Boot Application
Create a spring boot application and annotate with both@SpringBootApplication
and@EnableCaching
.
ProductApplication.java
package com.zheng.demo.openapi.products;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cache.annotation.EnableCaching;@SpringBootApplication@EnableCachingpublic class ProductsApplication { public static void main(String[] args) { SpringApplication.run(ProductsApplication.class, args); }}
3.2 Create Implementation Classes
In this step, I will create an implementation class for the generated interface with mocked data.
ProductsApiImpl.java
package com.zheng.demo.openapi.products.service;import java.math.BigDecimal;import java.util.Random;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.http.ResponseEntity;import org.springframework.stereotype.Component;import com.zheng.demo.openapi.products.api.ProductsApiDelegate;import com.zheng.demo.openapi.products.api.model.ProductDO;@Componentpublic class ProductsApiImpl implements ProductsApiDelegate {Logger logger = LoggerFactory.getLogger(ProductsApiImpl.class);private final Random rnd = new Random();@Overridepublic ResponseEntity<ProductDO> getProduct(BigDecimal id) {logger.info("getProduct called");ProductDO prod = new ProductDO();prod.setId(id);prod.setName("Product_" + id);prod.setPrice(BigDecimal.valueOf(100.0 + rnd.nextDouble() * 100.0));return ResponseEntity.ok(prod);}@Overridepublic ResponseEntity<ProductDO> createProduct(ProductDO product) {logger.info("createProduct called");product.setId(BigDecimal.valueOf(rnd.nextDouble() * 10));return ResponseEntity.ok(product);}}
- line 21, log a statement when
getProduct
service is called. It is used to verify if the cache is used or not. - line 32, log a statement when the
createProduct
service is called.
3.3 Application.yaml
Set the logging application properties and thebase-path
properties. We use the log statements to verify if the cache data is used or not.
application.yaml
logging: level: root: INFO org.springframework: INFOopenapi: product: base-path: v1
4. Generated Source Code
Execute themvn generate-source
command and review the generated java source files.
4.1 ApiUtil Class
TheApiUtil
class has only one static methodsetExampleResponse
.
ApiUtil.java
package com.zheng.demo.openapi.products.api;import org.springframework.web.context.request.NativeWebRequest;import jakarta.servlet.http.HttpServletResponse;import java.io.IOException;public class ApiUtil { public static void setExampleResponse(NativeWebRequest req, String contentType, String example) { try { HttpServletResponse res = req.getNativeResponse(HttpServletResponse.class); res.setCharacterEncoding("UTF-8"); res.addHeader("Content-Type", contentType); res.getWriter().print(example); } catch (IOException e) { throw new RuntimeException(e); } }}
Note: TheApiUtil.java
file has the same content when generated from both built-in template and custom template.
4.2 ProductsApi Interface
TheProductsApi
interface includes 3 defaults methods:
getDelegate
is from thedelegatePattern
setting.createProduct
andgetProduct
are defined in theProducts.yaml
file under theoperation
section.
ProductsApi.java
/** * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) (7.5.0). * https://openapi-generator.tech * Do not edit the class manually. */package com.zheng.demo.openapi.products.api;import java.math.BigDecimal;import com.zheng.demo.openapi.products.api.model.ProductDO;import org.springframework.http.ResponseEntity;import org.springframework.validation.annotation.Validated;import org.springframework.web.bind.annotation.*;import org.springframework.web.multipart.MultipartFile;import jakarta.validation.Valid;import jakarta.validation.constraints.*;import java.util.List;import java.util.Map;import jakarta.annotation.Generated;@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2024-05-04T23:36:23.308233300-05:00[America/Chicago]", comments = "Generator version: 7.5.0")@Validatedpublic interface ProductsApi { default ProductsApiDelegate getDelegate() { return new ProductsApiDelegate() {}; } /** * POST /products : Create a product * * @param productDO Create a new prodcut in the store (required) * @return OK (status code 200) */ @RequestMapping( method = RequestMethod.POST, value = "/products", produces = { "application/json" }, consumes = { "application/json" } ) default ResponseEntity<ProductDO> createProduct( @Valid @RequestBody ProductDO productDO ) { return getDelegate().createProduct(productDO); } /** * GET /products/{id} : Get product detail for a given product id * * @param id Product's identifier (required) * @return OK (status code 200) */ @RequestMapping( method = RequestMethod.GET, value = "/products/{id}", produces = { "application/json" } ) default ResponseEntity<ProductDO> getProduct( @PathVariable("id") BigDecimal id ) { return getDelegate().getProduct(id); }}
Note: TheProductsApi
file has the same content when generated from both built-in template and custom template.
4.3 ProductApiDelegate Interface
TheProductApiDelegate
interface has the same methods as theProductApi
interface.
ProductsApiDelegate.java
package com.zheng.demo.openapi.products.api;import java.math.BigDecimal;import com.zheng.demo.openapi.products.api.model.ProductDO;import org.springframework.http.HttpStatus;import org.springframework.http.MediaType;import org.springframework.http.ResponseEntity;import org.springframework.web.context.request.NativeWebRequest;import org.springframework.web.multipart.MultipartFile;import jakarta.validation.constraints.*;import jakarta.validation.Valid;import java.util.List;import java.util.Map;import java.util.Optional;import jakarta.annotation.Generated;/** * A delegate to be called by the {@link ProductsApiController}}. * Implement this interface with a {@link org.springframework.stereotype.Service} annotated class. */@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2024-05-04T23:36:23.308233300-05:00[America/Chicago]", comments = "Generator version: 7.5.0")public interface ProductsApiDelegate { default Optional<NativeWebRequest> getRequest() { return Optional.empty(); } /** * POST /products : Create a product * * @param productDO Create a new prodcut in the store (required) * @return OK (status code 200) * @see ProductsApi#createProduct */ default ResponseEntity<ProductDO> createProduct(ProductDO productDO) { getRequest().ifPresent(request -> { for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) { if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) { String exampleString = "{ \"price\" : 6.027456183070403, \"name\" : \"name\", \"id\" : 0.8008281904610115 }"; ApiUtil.setExampleResponse(request, "application/json", exampleString); break; } } }); return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); } /** * GET /products/{id} : Get product detail for a given product id * * @param id Product's identifier (required) * @return OK (status code 200) * @see ProductsApi#getProduct */ default ResponseEntity<ProductDO> getProduct(BigDecimal id) { getRequest().ifPresent(request -> { for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) { if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) { String exampleString = "{ \"price\" : 6.027456183070403, \"name\" : \"name\", \"id\" : 0.8008281904610115 }"; ApiUtil.setExampleResponse(request, "application/json", exampleString); break; } } }); return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); }}
Note: The OpenAPI built-in template does not know how to transform thex-spring-cacheable
extension, so developers must add the@org.springframework.cache.annotation.Cacheable("default")
annotation to the generated code in order to pass the integration test.
4.4 ProductApiController Class
The generatedProductsApiController
class has a “openapi.product.base-path
” property. If the property is not defined, then it falls back to an empty string""
. Please refer tostep 3.3 as thebase-path
value is set to “v1
“.
ProductsApiController.java
package com.zheng.demo.openapi.products.api;import java.math.BigDecimal;import com.zheng.demo.openapi.products.api.model.ProductDO;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.http.HttpStatus;import org.springframework.http.MediaType;import org.springframework.http.ResponseEntity;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestHeader;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.CookieValue;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RequestPart;import org.springframework.web.multipart.MultipartFile;import jakarta.validation.constraints.*;import jakarta.validation.Valid;import java.util.List;import java.util.Map;import java.util.Optional;import javax.annotation.Generated;@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2024-05-04T07:47:53.432603-05:00[America/Chicago]")@Controller@RequestMapping("${openapi.product.base-path:}")public class ProductsApiController implements ProductsApi { private final ProductsApiDelegate delegate; public ProductsApiController(@Autowired(required = false) ProductsApiDelegate delegate) { this.delegate = Optional.ofNullable(delegate).orElse(new ProductsApiDelegate() {}); } @Override public ProductsApiDelegate getDelegate() { return delegate; }}
Note: TheProductsApiController
file has the same content when generated from both built-in template and custom template.
4.5 ProductDO Class
The generatedProductDO
class defines theProduct
data model.
ProductDO.java
package com.zheng.demo.openapi.products.api.model;import java.net.URI;import java.util.Objects;import com.fasterxml.jackson.annotation.JsonProperty;import com.fasterxml.jackson.annotation.JsonCreator;import java.math.BigDecimal;import java.time.OffsetDateTime;import jakarta.validation.Valid;import jakarta.validation.constraints.*;import java.util.*;import jakarta.annotation.Generated;/** * Product Information */@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2024-05-04T23:36:23.308233300-05:00[America/Chicago]", comments = "Generator version: 7.5.0")public class ProductDO { private BigDecimal id; private String name; private BigDecimal price; public ProductDO id(BigDecimal id) { this.id = id; return this; } /** * product id * @return id */ @Valid @JsonProperty("id") public BigDecimal getId() { return id; } public void setId(BigDecimal id) { this.id = id; } public ProductDO name(String name) { this.name = name; return this; } /** * product name * @return name */ @JsonProperty("name") public String getName() { return name; } public void setName(String name) { this.name = name; } public ProductDO price(BigDecimal price) { this.price = price; return this; } /** * product price value * @return price */ @Valid @JsonProperty("price") public BigDecimal getPrice() { return price; } public void setPrice(BigDecimal price) { this.price = price; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } ProductDO productDO = (ProductDO) o; return Objects.equals(this.id, productDO.id) && Objects.equals(this.name, productDO.name) && Objects.equals(this.price, productDO.price); } @Override public int hashCode() { return Objects.hash(id, name, price); } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("class ProductDO {\n"); sb.append(" id: ").append(toIndentedString(id)).append("\n"); sb.append(" name: ").append(toIndentedString(name)).append("\n"); sb.append(" price: ").append(toIndentedString(price)).append("\n"); sb.append("}"); return sb.toString(); } /** * Convert the given object to string with each line indented by 4 spaces * (except the first line). */ private String toIndentedString(Object o) { if (o == null) { return "null"; } return o.toString().replace("\n", "\n "); }}
5. Junit Test Classes
5.1 Unit Test
ProductsApiImplUnitTest
has 2 tests which test bothgetProduct
andcreateProduct
services.
ProductsApiImplUnitTest.java
package com.zheng.demo.openapi.products.service;import static org.assertj.core.api.Assertions.assertThat;import java.math.BigDecimal;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.http.ResponseEntity;import com.zheng.demo.openapi.products.api.ProductsApi;import com.zheng.demo.openapi.products.api.model.ProductDO;@SpringBootTestclass ProductsApiImplUnitTest {@Autowiredprivate ProductsApi api;@Testvoid whenGetProduct_then_success() {ResponseEntity<ProductDO> response = api.getProduct(new BigDecimal(1));assertThat(response).isNotNull();assertThat(response.getStatusCode().is2xxSuccessful()).isTrue();}@Testvoid whenCreateProduct_then_success() {ProductDO product = new ProductDO();product.setName("Test");product.setPrice(new BigDecimal(100));ResponseEntity<ProductDO> response = api.createProduct(product);assertThat(response).isNotNull();assertThat(response.getStatusCode().is2xxSuccessful()).isTrue();}}
Execute the junit tests and both passed as expected.
5.2 Integration Test
ProductsApplicationIntegrationTest
has 3 tests which test bothgetProduct
andcreateProduct
at the test server. ThegetProduct
should use cache for the same product id.
ProductsApplicationIntegrationTest.java
package com.zheng.demo.openapi.products;import static org.assertj.core.api.Assertions.assertThat;import java.util.stream.Collectors;import java.util.stream.IntStream;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.boot.test.web.client.TestRestTemplate;import org.springframework.boot.test.web.server.LocalServerPort;import org.springframework.http.HttpEntity;import org.springframework.http.HttpStatus;import org.springframework.http.ResponseEntity;import com.zheng.demo.openapi.products.api.model.ProductDO;@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)class ProductsApplicationIntegrationTest {@LocalServerPortprivate int port;@Autowiredprivate TestRestTemplate restTemplate;@Testvoid whenGetProduct_thenSuccess() {ResponseEntity<ProductDO> response = restTemplate.getForEntity("http://localhost:" + port + "/v1/products/1",ProductDO.class);assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);}@Testvoid whenGetProductMultipleTimes_thenResponseCached() {// Call server a few times and collect responsesvar quotes = IntStream.range(1, 10).boxed().map((i) -> restTemplate.getForEntity("http://localhost:" + port + "/v1/products/1", ProductDO.class)).map(HttpEntity::getBody).collect(Collectors.groupingBy((q -> q.hashCode()), Collectors.counting()));assertThat(quotes.size()).isEqualTo(1);}@Testvoid whenCreateProduct_thenSuccess() {ProductDO product = new ProductDO();product.setName("TEST");ResponseEntity<ProductDO> response = restTemplate.postForEntity("http://localhost:" + port + "/v1/products",product, ProductDO.class);assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);}}
ThewhenGetProductMultipleTimes_thenResponseCached
test failed as thegetProduct
service was called multiple times.
2024-05-04T23:28:54.048-05:00 INFO 36452 --- [o-auto-1-exec-2] c.z.d.o.p.service.ProductsApiImpl : createProduct called2024-05-04T23:28:54.150-05:00 INFO 36452 --- [o-auto-1-exec-5] c.z.d.o.p.service.ProductsApiImpl : getProduct called2024-05-04T23:28:54.164-05:00 INFO 36452 --- [o-auto-1-exec-4] c.z.d.o.p.service.ProductsApiImpl : getProduct called2024-05-04T23:28:54.168-05:00 INFO 36452 --- [o-auto-1-exec-1] c.z.d.o.p.service.ProductsApiImpl : getProduct called2024-05-04T23:28:54.173-05:00 INFO 36452 --- [o-auto-1-exec-3] c.z.d.o.p.service.ProductsApiImpl : getProduct called2024-05-04T23:28:54.178-05:00 INFO 36452 --- [o-auto-1-exec-6] c.z.d.o.p.service.ProductsApiImpl : getProduct called2024-05-04T23:28:54.181-05:00 INFO 36452 --- [o-auto-1-exec-7] c.z.d.o.p.service.ProductsApiImpl : getProduct called2024-05-04T23:28:54.185-05:00 INFO 36452 --- [o-auto-1-exec-8] c.z.d.o.p.service.ProductsApiImpl : getProduct called2024-05-04T23:28:54.189-05:00 INFO 36452 --- [o-auto-1-exec-9] c.z.d.o.p.service.ProductsApiImpl : getProduct called2024-05-04T23:28:54.193-05:00 INFO 36452 --- [-auto-1-exec-10] c.z.d.o.p.service.ProductsApiImpl : getProduct called2024-05-04T23:28:54.196-05:00 INFO 36452 --- [o-auto-1-exec-2] c.z.d.o.p.service.ProductsApiImpl : getProduct called
Note: thex-spring-cacheable
is not supported by the default template, therefore thegetProduct
service is called 10 times instead of one time.
6. Spring Boot OpenAPI Generator Custom Templates
As seen instep 5, the generated spring boot server stub source code missed the cache annotation. In this step, I will update the built-inapiDelegate.mustache
template to add the “x-spring-cacheable
” vendor extension and use it when generating the code from theproducts1.yaml
.
/** Generated code: do not modify !* Custom template with support for x-spring-cacheable extension*/package {{package}};{{#imports}}import {{import}};{{/imports}}import org.springframework.http.HttpStatus;import org.springframework.http.MediaType;{{#useResponseEntity}} import org.springframework.http.ResponseEntity;{{/useResponseEntity}}import org.springframework.web.context.request.NativeWebRequest;import org.springframework.web.multipart.MultipartFile;{{#reactive}} import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.http.codec.multipart.Part;{{/reactive}}{{#useBeanValidation}} import {{javaxPackage}}.validation.constraints.*; import {{javaxPackage}}.validation.Valid;{{/useBeanValidation}}import java.util.List;import java.util.Map;import java.util.Optional;{{#async}} import java.util.concurrent.CompletableFuture;{{/async}}import {{javaxPackage}}.annotation.Generated;{{#operations}} /** * A delegate to be called by the {@link {{classname}}Controller}}. * Implement this interface with a {@link org.springframework.stereotype.Service} annotated class. */ {{>generatedAnnotation}} public interface {{classname}}Delegate { {{#jdk8-default-interface}} default Optional<NativeWebRequest> getRequest() { return Optional.empty(); } {{/jdk8-default-interface}} {{#operation}} /** * {{httpMethod}} {{{path}}}{{#summary}} : {{.}}{{/summary}} {{#notes}} * {{.}} {{/notes}} * {{#allParams}} * @param {{paramName}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}} {{/allParams}} * @return {{#responses}}{{message}} (status code {{code}}){{^-last}} * or {{/-last}}{{/responses}} {{#isDeprecated}} * @deprecated {{/isDeprecated}} {{#externalDocs}} * {{description}} * @see <a href="{{url}}">{{summary}} Documentation</a> {{/externalDocs}} * @see {{classname}}#{{operationId}} */ {{#isDeprecated}} @Deprecated {{/isDeprecated}} {{#vendorExtensions.x-spring-cacheable}} @org.springframework.cache.annotation.Cacheable({{#name}}"{{.}}"{{/name}}{{^name}}"default"{{/name}}) {{/vendorExtensions.x-spring-cacheable}} {{#jdk8-default-interface}}default {{/jdk8-default-interface}}{{>responseType}} {{operationId}}({{#allParams}}{{^isFile}}{{^isBodyParam}}{{>optionalDataType}}{{/isBodyParam}}{{#isBodyParam}}{{^reactive}}{{{dataType}}}{{/reactive}}{{#reactive}}{{^isArray}}Mono<{{{dataType}}}>{{/isArray}}{{#isArray}}Flux<{{{baseType}}}>{{/isArray}}{{/reactive}}{{/isBodyParam}}{{/isFile}}{{#isFile}}{{#isArray}}List<{{/isArray}}{{#reactive}}Flux<Part>{{/reactive}}{{^reactive}}MultipartFile{{/reactive}}{{#isArray}}>{{/isArray}}{{/isFile}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{#reactive}}{{#hasParams}}, {{/hasParams}}ServerWebExchange exchange{{/reactive}}{{#vendorExtensions.x-spring-paginated}}{{#hasParams}}, {{/hasParams}}{{^hasParams}}{{#reactive}}, {{/reactive}}{{/hasParams}}final Pageable pageable{{/vendorExtensions.x-spring-paginated}}){{#unhandledException}} throws Exception{{/unhandledException}}{{^jdk8-default-interface}};{{/jdk8-default-interface}}{{#jdk8-default-interface}} { {{>methodBody}} }{{/jdk8-default-interface}} {{/operation}} }{{/operations}}
- line 73-75: support the
x-spring-cacheable
vendor-specific extension.
6.1 Use Products1.yaml
Update thepom.xml
to specifytemplateResourcePath
with the custom template and useproducts1.xml
. As you seen atstep 2, theproducts1.yaml
includesx-spring-cacheable: true
Updated pom.xml at the configuration section
<configuration><inputSpec>${project.basedir}/src/main/resources/api/products1.yaml</inputSpec><generatorName>spring</generatorName><supportingFilesToGenerate>ApiUtil.java</supportingFilesToGenerate><templateResourcePath>${project.basedir}/src/main/resources/templates/JavaSpring</templateResourcePath><globalProperties><debugOpenAPI>true</debugOpenAPI></globalProperties><configOptions><delegatePattern>true</delegatePattern><apiPackage>com.zheng.demo.openapi.products.api</apiPackage><modelPackage>com.zheng.demo.openapi.products.api.model</modelPackage><documentationProvider>source</documentationProvider><dateLibrary>java8</dateLibrary><openApiNullable>false</openApiNullable><useJakartaEe>true</useJakartaEe><useSpringBoot3>true</useSpringBoot3></configOptions>
Note: line 7 specifies thetemplateResourcePath
file location.
6.2 Re-generate the Source
Run themvn generate-source
command and it will generate five files as thestep 4. All files have the same content exceptProductsApiDelegate
now added@org.springframework.cache.annotation.Cacheable("default")
to the getProduct method.
ProductsApiDelegate.java
‘sgetProduct
method
@org.springframework.cache.annotation.Cacheable("default") default ResponseEntity<ProductDO> getProduct(BigDecimal id) { getRequest().ifPresent(request -> { for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) { if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) { String exampleString = "{ \"price\" : 6.027456183070403, \"name\" : \"name\", \"id\" : 0.8008281904610115 }"; ApiUtil.setExampleResponse(request, "application/json", exampleString); break; } } }); return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); }
Note: line 1, the@org.springframework.cache.annotation.Cacheable("default")
is added.
7. Using the Modified Template
In this step, I will update thepom.xml
to specify the template file and then re-generate the source code. this time, it will include the@cacheable
annotation.
7.1 Use Products2.yaml
Update thepom.xml
at theinputSpec
to useproducts2.yaml
. Refer tostep 2,products2.yaml
specifies thex-spring-cacheable: name: get-product
7.2 Re-generate the Source
@org.springframework.cache.annotation.Cacheable("get-product") default ResponseEntity<ProductDO> getProduct(BigDecimal id) { getRequest().ifPresent(request -> { for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) { if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) { String exampleString = "{ \"price\" : 6.027456183070403, \"name\" : \"name\", \"id\" : 0.8008281904610115 }"; ApiUtil.setExampleResponse(request, "application/json", exampleString); break; } } }); return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); }
Note: line 1, the@org.springframework.cache.annotation.Cacheable("get-product")
is added.
Run tests and now all passed as the cache is used.
8. Conclusion
In this tutorial, I demonstrated how to configure the OpenAPI Generator tool in a spring boot maven project and generate source code from an open API specification with a custom template that supports a simple vendor extension. The spring boot openapi generator custom templates are used in the following steps.
- Execute the maven
generate-source
command to generate source code from theproducts.yaml
file. - Update the
products.yaml
file with the “x-spring-cacheable
” vendor-specific extension with the default setting and generate from a custom template. - Update the
products.yaml
file with the “x-spring-cacheable
” vendor-specific extension and set the cache name and generate from a custom template.
9. Download
This was an example of Spring boot maven project which generates source code from a custom template.
You can download the full source code of this example here:OpenAPI Generator Custom Templates

Thank you!
We will contact you soon.