1. Introduction
OpenAPI Generator is a tool that allows us to quickly generate client and server code from REST API definitions, supporting multiple languages and frameworks. Although most of the time the generated code is ready to be used with no modifications, there may be scenarios in which we need to customize it.
In this tutorial, we’ll learn how to use custom templates to address these scenarios.
2. OpenAPI Generator Project Setup
Before exploring customization, let’s run through a quick overview of a typical usage scenario for this tool: generating server-side code from a given API definition. We assume we already have a base Spring Boot MVC application built with Maven, so we’ll use the appropriate plugin for that:
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>7.7.0</version>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<inputSpec>${project.basedir}/src/main/resources/api/quotes.yaml</inputSpec>
<generatorName>spring</generatorName>
<supportingFilesToGenerate>ApiUtil.java</supportingFilesToGenerate>
<templateResourcePath>${project.basedir}/src/templates/JavaSpring</templateResourcePath>
<configOptions>
<dateLibrary>java8</dateLibrary>
<openApiNullable>false</openApiNullable>
<delegatePattern>true</delegatePattern>
<apiPackage>com.baeldung.tutorials.openapi.quotes.api</apiPackage>
<modelPackage>com.baeldung.tutorials.openapi.quotes.api.model</modelPackage>
<documentationProvider>source</documentationProvider>
</configOptions>
</configuration>
</execution>
</executions>
</plugin>
With this configuration, the generated code will go into the target/generated-sources/openapi folder. Moreover, our project also needs to add a dependency to the OpenAPI V3 annotation library:
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId>
<version>2.2.3</version>
</dependency>
The latest versions of the plugins and this dependency are available on Maven Central:
The API for this tutorial consists of a single GET operation that returns a quote for a given financial instrument symbol:
openapi: 3.0.0
info:
title: Quotes API
version: 1.0.0
servers:
- description: Test server
url: http://localhost:8080
paths:
/quotes/{symbol}:
get:
tags:
- quotes
summary: Get current quote for a security
operationId: getQuote
parameters:
- name: symbol
in: path
required: true
description: Security's symbol
schema:
type: string
pattern: '[A-Z0-9]+'
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/QuoteResponse'
components:
schemas:
QuoteResponse:
description: Quote response
type: object
properties:
symbol:
type: string
description: security's symbol
price:
type: number
description: Quote value
timestamp:
type: string
format: date-time
Even without any written code, the resulting project can already serve API calls thanks to the default implementation of the QuotesApi – although it will always return a 502 error since the method is not implemented.
3. API Implementation
The next step is to code an implementation of the QuotesApiDelegate interface. Since we’re using a delegate pattern, we don’t need to worry about MVC or OpenAPI-specific annotations, as those will be kept apart in the generated controller.
This approach ensures that, if we later decide to add a library like SpringDoc or similar to the project, the annotations upon which those libraries depend will always be in sync with the API definition. Another benefit is that contract modifications will also change the delegate interface, thus making the project unbuildable. This is good, as it minimizes runtime errors that can happen in code-first approaches.
In our case, the implementation consists of a single method that uses a BrokerService to retrieve quotes:
@Component
public class QuotesApiImpl implements QuotesApiDelegate {
// ... fields and constructor omitted
@Override
public ResponseEntity<QuoteResponse> getQuote(String symbol) {
var price = broker.getSecurityPrice(symbol);
var quote = new QuoteResponse();
quote.setSymbol(symbol);
quote.setPrice(price);
quote.setTimestamp(OffsetDateTime.now(clock));
return ResponseEntity.ok(quote);
}
}
We also inject a Clock to provide the timestamp field required by the returned QuoteResponse. This is a small implementation detail that makes it easier to unit-test code that uses the current time. For instance, we can simulate the behavior of the code under test at a specific point in time using Clock.fixed(). The unit test for the implementation class uses this approach.
Finally, we’ll implement a BrokerService that simply returns a random quote, which is enough for our purposes.
We can verify that this code works as expected by running the integration test:
@Test
void whenGetQuote_thenSuccess() {
var response = restTemplate.getForEntity("http://localhost:" + port + "/quotes/BAEL", QuoteResponse.class);
assertThat(response.getStatusCode())
.isEqualTo(HttpStatus.OK);
}
4. OpenAPI Generator Customization Scenario
So far, we’ve implemented a service with no customization. Let’s consider the following scenario: As an API definition author, I’d like to specify that a given operation may return a cached result. The OpenAPI specification allows this kind of non-standard behavior through a mechanism called vendor extensions, which can be applied to many (but not all) elements.
For our example, we’ll define an x-spring-cacheable extension to be applied on any operation we want to have this behavior. This is the modified version of our initial API with this extension applied:
# ... other definitions omitted
paths:
/quotes/{symbol}:
get:
tags:
- quotes
summary: Get current quote for a security
operationId: getQuote
x-spring-cacheable: true
parameters:
# ... more definitions omitted
Now, if we run the generator again with mvn generate-sources, nothing will happen. This is expected because, although still valid, the generator doesn’t know what to do with this extension. More precisely, the templates used by the generator don’t make any use of the extension.
Upon closer examination of the generated code, we see that we can achieve our goal by adding a @Cacheable annotation on the delegate interface methods that match API operations having our extension. Let’s explore how to do this next.
4.1. Customization Options
The OpenAPI Generator tool supports two customization approaches:
- Adding a new custom generator, created from scratch or by extending an existing one
- Replacing templates used by an existing generator with a custom one
The first option is more “heavy-weight” but allows full control of the artifacts generated. It’s the only option when our goal is to support code generation for a new framework or language, but we’ll not cover it here.
For now, all we need is to change a single template, which is the second option. The first step, then, is to find this template. The official documentation recommends using the CLI version of the tool to extract all templates for a given generator.
However, when using the Maven plugin, it’s usually more convenient to look it up directly on the GitHub repository. Notice that, to ensure compatibility, we’ve picked the source tree for the tag that corresponds to the plugin version in use.
In the resources folder, each sub-folder has templates used for a specific generator target. For Spring-based projects, the folder name is JavaSpring. There, we’ll find the Mustache templates used to render the server code. Most templates are named sensibly, so it’s not hard to figure out which one we need: apiDelegate.mustache.
4.2. Template Customization
Once we’ve located the templates we want to customize, the next step is to place them in our project so the Maven plugin can use them. We’ll put the soon-to-customize template under the folder src/templates/JavaSpring so that it doesn’t get mixed with other sources or resources.
Next, we need to add a configuration option to the plugin informing about our directory:
<configuration>
<inputSpec>${project.basedir}/src/main/resources/api/quotes.yaml</inputSpec>
<generatorName>spring</generatorName>
<supportingFilesToGenerate>ApiUtil.java</supportingFilesToGenerate>
<templateResourcePath>${project.basedir}/src/templates/JavaSpring</templateResourcePath>
... other unchanged properties omitted
</configuration>
To verify that the generator is correctly configured, let’s add a comment on top of the template and re-generate the code:
/*
* Generated code: do not modify !
* Custom template with support for x-spring-cacheable extension
*/
package {{package}};
... more template code omitted
Next, running mvn clean generate-sources will yield a new version of the QuotesDelegateApi with the comment:
/*
* Generated code: do not modify!
* Custom template with support for x-spring-cacheable extension
*/
package com.baeldung.tutorials.openapi.quotes.api;
... more code omitted
This shows that the generator picked our custom template instead of the native one.
4.3. Exploring the Base Template
Now, let’s take a look at our template to find the proper place to add our customization. We can see that there’s a section defined by the {{#operation}} {{/operation}} tags that outputs the delegate’s methods in the rendered class:
{{#operation}}
// ... many mustache tags omitted
{{#jdk8-default-interface}}default // ... more template logic omitted
{{/operation}}
Inside this section, the template uses several properties of the current context – an operation – to generate the corresponding method’s declaration.
In particular, we can find information about vendor extensions under {{vendorExtension}}. This is a map where the keys are extension names, and the value is a direct representation of whatever data we’ve put in the definition. This means we can use extensions where the value is an arbitrary object or just a simple string.
To get a JSON representation of the full data structure that the generator passes to the template engine, add the following globalProperties element to the plugin’s configuration:
<configuration>
<inputSpec>${project.basedir}/src/main/resources/api/quotes.yaml</inputSpec>
<generatorName>spring</generatorName>
<supportingFilesToGenerate>ApiUtil.java</supportingFilesToGenerate>
<templateResourcePath>${project.basedir}/src/templates/JavaSpring</templateResourcePath>
<globalProperties>
<debugOpenAPI>true</debugOpenAPI>
<debugOperations>true</debugOperations>
</globalProperties>
...more configuration options omitted
Now, when we run mvn generate-sources again, the output will have this JSON representation right after the message ## Operation Info##:
[INFO] ############ Operation info ############
[ {
"appVersion" : "1.0.0",
... many, many lines of JSON omitted
4.4. Adding @Cacheable to Operations
We’re now ready to add the required logic to support caching operation results. One aspect that might be useful is to allow users to specify a cache name, but not require them to do so.
To support this requirement, we’ll support two variants of our vendor extension. If the value is simply true, a default cache name will be used:
paths:
/some/path:
get:
operationId: getSomething
x-spring-cacheable: true
Otherwise, it will expect an object with a name property that we’ll use as the cache name:
paths:
/some/path:
get:
operationId: getSomething
x-spring-cacheable:
name: mycache
This is how the modified template looks with the required logic to support both variants:
{{#vendorExtensions.x-spring-cacheable}}
@org.springframework.cache.annotation.Cacheable({{#name}}"{{.}}"{{/name}}{{^name}}"default"{{/name}})
{{/vendorExtensions.x-spring-cacheable}}
{{#jdk8-default-interface}}default // ... template logic omitted
We’ve added the logic to add the annotation right before the method’s signature definition. Notice the use of {{#vendorExtensions.x-spring-cacheable}} to access the extension value. According to Mustache rules, the inner code will be executed only if the value is “truthy”, meaning something that evaluates to true in a Boolean context. Despite this somewhat loose definition, it works fine here and is quite readable.
As for the annotation itself, we’ve opted to use “default” for the default cache name. This allows us to further customize the cache, although the details on how to do this are outside the scope of this tutorial.
5. Using the Modified Template
Finally, let’s modify our API definition to use our extension:
... more definitions omitted
paths:
/quotes/{symbol}:
get:
tags:
- quotes
summary: Get current quote for a security
operationId: getQuote
x-spring-cacheable: true
name: get-quotes
Let’s run mvn generate-sources once again to create a new version of QuotesApiDelegate:
... other code omitted
@org.springframework.cache.annotation.Cacheable("get-quotes")
default ResponseEntity<QuoteResponse> getQuote(String symbol) {
... default method's body omitted
We see that the delegate interface now has the @Cacheable annotation. Moreover, we see that the cache name corresponds to the name attribute from the API definition.
Now, for this annotation to have any effect, we also need to add the @EnableCaching annotation to a @Configuration class or, as in our case, to the main class:
@SpringBootApplication
@EnableCaching
public class QuotesApplication {
public static void main(String[] args) {
SpringApplication.run(QuotesApplication.class, args);
}
}
To verify that the cache is working as expected, let’s write an integration test that calls the API multiple times:
@Test
void whenGetQuoteMultipleTimes_thenResponseCached() {
var quotes = IntStream.range(1, 10).boxed()
.map((i) -> restTemplate.getForEntity("http://localhost:" + port + "/quotes/BAEL", QuoteResponse.class))
.map(HttpEntity::getBody)
.collect(Collectors.groupingBy((q -> q.hashCode()), Collectors.counting()));
assertThat(quotes.size()).isEqualTo(1);
}
We expect all responses to return identical values, so we’ll collect them and group them by their hash codes. If all responses produce the same hash code, then the resulting map will have a single entry. Note that this strategy works because the generated model class implements the hashCode() method using all fields.
6. Conclusion
In this article, we’ve shown how to configure the OpenAPI Generator tool to use a custom template that adds support for a simple vendor extension.
As usual, all code is available over on GitHub.