OpenAPI From Code With Spring and Java: A Recipe for Your CI
Have you ever needed to generate OpenAPI documentation directly from your code and, more importantly, do it in a way that fits cleanly into a CI pipeline?
Join the DZone community and get the full member experience.
Join For FreeThis is not "just another article about Springdoc," I promise. This is a ready-to-use recipe I was struggling to find one day, and had to build it from scratch.
Have you ever needed to generate OpenAPI documentation directly from your code and, more importantly, do it in a way that fits cleanly into a CI pipeline? Swagger UI is commonly used in Spring Boot applications to visualize and test APIs from the browser. It can also expose the generated OpenAPI definition through a configurable endpoint, and that endpoint is exactly what we will use in this article.
Why OpenAPI Documentation Matters
Frontend Client Generation
One of the most practical uses of OpenAPI documentation is automatic client generation. Tools such as OpenAPI Generator or Swagger Codegen can take an OpenAPI definition and produce TypeScript, JavaScript, or Java clients with very little manual effort.
Mocking a Service Before It Is Ready
In early development stages, a team may want to spin up a mock server before the real endpoints are fully implemented. Tools such as Mockoon or WireMock can use an OpenAPI specification to simulate the service. This is especially useful for frontend teams that need to move forward while backend work is still in progress.
Verifying Contracts Between Services
When multiple services depend on one another, compatibility becomes critical. OpenAPI documentation can be used together with tools such as Spring Cloud Contract to verify that both providers and consumers still conform to the agreed contract.
The Manual Approach to Generating OpenAPI Documentation
Let us start with a simple Spring Boot project. Add the following dependencies to pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.6.0</version>
</dependency>
Then add Springdoc configuration to application.yml:
springdoc:
api-docs:
path: /api-docs
enabled: true
swagger-ui:
url: /api-docs
enabled: true
Now create a simple REST controller:
@RestController
@Tag(name = "default", description = "General API")
@RequestMapping("/api/v1/default")
public class WebRestController {
private static final Logger log =
LoggerFactory.getLogger(WebRestController.class);
@GetMapping(produces = MediaType.TEXT_PLAIN_VALUE)
@ResponseStatus(HttpStatus.OK)
public String get() {
log.info("GET method called");
return "Hello!";
}
@PostMapping(
consumes = MediaType.TEXT_PLAIN_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE
)
@ResponseStatus(HttpStatus.OK)
public Set<String> post(@RequestBody String body) {
log.info("POST method called");
return Set.of(body);
}
Finally, add a security configuration that allows access to both the REST API and to Swagger UI:
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class WebSecurityConfig {
@Profile("!openapi")
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity.authorizeHttpRequests(
request -> request
.requestMatchers("/api-docs", "/api-docs/**").permitAll()
.requestMatchers("/swagger-ui/*").permitAll()
.requestMatchers("/api/v1/default").permitAll()
.requestMatchers("/**").authenticated()
)
.csrf(CsrfConfigurer::disable)
.build();
}
@Profile("openapi")
@Bean
public SecurityFilterChain filterChainOpenApi(HttpSecurity httpSecurity) throws Exception {
return httpSecurity.authorizeHttpRequests(
request -> request.anyRequest().permitAll()
)
.csrf(CsrfConfigurer::disable)
.build();
}
Notice the separate openapi profile. We will use it later during automated generation.
At this point, you can run the application and open Swagger UI at http://localhost:8080/swagger-ui/index.html. From there, the generated OpenAPI document is available at http://localhost:8080/api-docs. You can save that response manually and use it as your specification file.
This works, but it is repetitive and not very practical for build automation. So let us move to the more useful approach: generating the spec during the Maven build.
Automatic Generation
To generate an OpenAPI file automatically, it helps to understand what actually happens during the build.
The springdoc-openapi-maven-plugin does not generate the specification out of thin air. It calls the application endpoint that exposes the OpenAPI definition. In other words, your Spring Boot application must be running while the plugin executes. That is why the spring-boot-maven-plugin and springdoc-openapi-maven-plugin are typically used together.
Because the application has to be started during the build, the security configuration must also allow the documentation endpoint to be accessed in that scenario. This is exactly why the separate openapi Spring profile is useful.
Add a Dedicated Maven Profile
Add the following Maven profile to pom.xml:
<profile>
<id>openapi</id>
<properties>
<maven.test.skip>true</maven.test.skip>
</properties>
<build>
<plugins>
<!-- When the Maven profile is openapi, run Spring with the openapi profile -->
<plugin>
<artifactId>spring-boot-maven-plugin</artifactId>
<groupId>org.springframework.boot</groupId>
<configuration>
<jvmArguments>
-Dspring.application.admin.enabled=true -Dspring.profiles.active=openapi
</jvmArguments>
</configuration>
<executions>
<execution>
<id>pre-integration-test</id>
<goals>
<goal>start</goal>
</goals>
</execution>
<execution>
<id>post-integration-test</id>
<goals>
<goal>stop</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- Generate the OpenAPI file during the build -->
<plugin>
<artifactId>springdoc-openapi-maven-plugin</artifactId>
<groupId>org.springdoc</groupId>
<version>1.4</version>
<configuration>
<skip>false</skip>
<apiDocsUrl>http://localhost:8080/api-docs.yaml</apiDocsUrl>
<outputDir>${project.build.directory}</outputDir>
<outputFileName>openapi.yml</outputFileName>
</configuration>
<executions>
<execution>
<id>integration-test</id>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
The important parts here are:
- We create
openapiMaven andopenapiSpring profiles, but they are not the same (and should not necessarily have those exact names or share one name). - When
openapiMaven profile is run, we run Spring app withopenapiprofile (look atjvmArguments) -Dspring.profiles.active=openapienables the relaxed security profile created specifically for documentation generation.apiDocsUrlpoints to the endpoint that returns the OpenAPI document.outputDirandoutputFileNamecontrol where the generated file is written.
These are the exact parts I struggled to find in one place, hence the "recipe" article.
Run the Generation Step
Once the profile is in place, generating the spec is easy:
./mvnw verify -Popenapi
After the build completes, the generated OpenAPI spec should be here:
./target/openapi.yml
Using It in a CI Pipeline
This setup is CI-friendly because the same command can run locally and in your pipeline:
./mvnw verify -Popenapi
From there you can archive target/openapi.yml as a build artifact, publish it to an artifact repository, pass it to frontend code generators, mock servers, and contract verification jobs.
Conclusion
Generating OpenAPI documentation manually from Swagger UI is fine for quick inspection, but it does not scale well when you need repeatability. By wiring Spring Boot and Springdoc into a dedicated Maven profile, you can generate the specification automatically during the build in your CI. That gives you a reliable OpenAPI artifact that can support client generation, service mocking, and contract verification without adding a separate manual step to the development workflow.
Bonus: Represent Set as an Array
In some cases, you may want a Set to be represented as a regular array in the generated OpenAPI specification instead of an array with uniqueItems: true. This can be useful when downstream tools expect a plain array schema (this is the exact request I once got from the frontend team).
You can customize Springdoc behavior with a small configuration class:
import org.springdoc.core.utils.SpringDocUtils;
import io.swagger.v3.oas.models.media.Schema;
import java.util.Collections;
import java.util.Set;
public class SwaggerConfig {
// Make springdoc generate an Array schema for Set.class
// and remove uniqueItems: true
public SwaggerConfig() {
var schema = new Schema<Set<?>>();
schema.type("array").example(Collections.emptyList());
SpringDocUtils.getConfig().replaceWithSchema(Set.class, schema);
}
With this adjustment in place, the generated schemas for Set will be emitted as an array, which can simplify integration with some client generators and consumers.
Opinions expressed by DZone contributors are their own.
Comments