OpenAPI Initiative is a widely adopted industry standard to describe and document APIs, with Swagger being one of its most well-known implementations. For years, Springfox, using Swagger, has provided a well-adopted toolchain for Spring projects to generate OpenAPI documentation and provide a UI on the top of it. Unfortunately, the Springfox project is not frequently maintained; its latest release v2.9.2 at the timing of writing this post was in 2018. This is where springdoc-openapi comes into the picture.
Springdoc is a relatively young open-source project that adds several new features not available in Springfox at the moment, including the support for OpenAPI Specification 3 (OAS 3) and functional and reactive Spring APIs to create REST endpoints. In this post, we’ll explore how we can use Springdoc with a Spring Boot project.
You can run an instance of Postgres by installing it on your machine or in the cloud. For a Docker container, use the following Compose
file.
version: '3'
services:
db:
image: postgres:13-alpine
container_name: pg13
restart: always
ports:
5432:5432
- environment:
POSTGRES_USER: erin
POSTGRES_PASSWORD: richards
Execute the following command to launch the container.
docker-compose up -d
Springdoc with Spring WebMvc
Generate a Spring Boot project using Spring Initializr, and add spring-boot-starter-web
, spring-boot-starter-data-jdbc
, postgresql
, and spring-boot-starter-actuator
dependencies. Your pom.xml
would look like this.
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 https://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.3.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
<parent>
</
groupId>dev.mflash.guides</groupId>
<artifactId>springdoc-integration</artifactId>
<version>0.0.1-SNAPSHOT</version>
<
properties>
<java.version>14</java.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-starter-data-jdbc</artifactId>
<dependency>
</dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
<dependency>
</
dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<dependency>
</
dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<exclusion>
</exclusions>
</dependency>
</dependencies>
</
build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<plugin>
</plugins>
</build>
</
project> </
Rename application.properties
to application.yml
, open it in an editor, and add the following configuration (change it wherever required).
# src/main/resources/application.yml
spring:
datasource:
platform: postgres
url: jdbc:postgresql://localhost:5432/spring
username: erin
password: richards
Create some endpoints
Let’s quickly create some endpoints. Say we want to save a Note
object in a Postgres relation defined by the following statement.
CREATE TABLE note (
SERIAL PRIMARY KEY,
id TEXT,
title TEXT
content );
Define an entity for this relation as follows.
// src/main/java/dev/mflash/guides/springdoc/Note.java
public class Note {
private @Id long id;
private String title;
private String content;
// getters, setters, etc
}
The id
will be automatically generated by a Postgres sequence that gets created with the CREATE
statement above which specifies the id
field to be of SERIAL
type.
Create a repository to perform CRUD operations with the Note
entity.
// src/main/java/dev/mflash/guides/springdoc/NoteRepository.java
public interface NoteRepository extends CrudRepository<Note, Long> {
}
Expose some of the CRUD operations through a controller.
// src/main/java/dev/mflash/guides/springdoc/NoteController.java
@RestController
@RequestMapping("/note")
public class NoteController {
private final NoteRepository repository;
public NoteController(NoteRepository repository) {
this.repository = repository;
}
@PutMapping
public List<Note> save(@RequestBody List<Note> notes) {
List<Note> savedNotes = new ArrayList<>();
.saveAll(notes).forEach(savedNotes::add);
repositoryreturn savedNotes;
}
@GetMapping
public List<Note> findAll() {
List<Note> savedNotes = new ArrayList<>();
.findAll().forEach(savedNotes::add);
repositoryreturn savedNotes;
}
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public List<Note> upload(@RequestParam("data") MultipartFile csv) throws IOException {
List<Note> savedNotes = new ArrayList<>();
List<Note> notes = new BufferedReader(
new InputStreamReader(Objects.requireNonNull(csv).getInputStream(), StandardCharsets.UTF_8)).lines()
Note::parseNote).collect(Collectors.toList());
.map(.saveAll(notes).forEach(savedNotes::add);
repositoryreturn savedNotes;
}
@DeleteMapping("/{id}")
public boolean delete(@PathVariable("id") long id) {
.deleteById(id);
repositoryreturn true;
} }
Here, apart from the usual endpoints, we also want to upload a CSV of notes and persist them in the database; a functionality exposed through the upload
method.
Integrating Springdoc
Add the following dependency in the pom.xml
.
dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.4.3</version>
<dependency> </
Add a @Tag
to NoteController
to describe it.
// src/main/java/dev/mflash/guides/springdoc/NoteController.java
@Tag(name = "Note", description = "Endpoints for CRUD operations on notes")
public class NoteController {
// rest of the code
}
To add some metadata, inject a bean returning an OpenAPI
object.
// src/main/java/dev/mflash/guides/springdoc/Launcher.java
public @SpringBootApplication class Launcher {
public static void main(String[] args) {
SpringApplication.run(Launcher.class, args);
}
public @Bean OpenAPI noteAPI() {
return new OpenAPI()
.info(new Info()
"Note API")
.title("A CRUD API to demonstrate Springdoc integration")
.description("0.0.1-SNAPSHOT")
.version(
.license(new License().name("MIT").url("https://opensource.org/licenses/MIT")
)
);
} }
Launch the application and open http://localhost:8080/swagger-ui.html. You’d see the Swagger UI with the endpoints exposed by NoteController
.

You can also access the OpenAPI docs at http://localhost:8080/v3/api-docs which can be imported in tools like Postman, Insomnia, etc.
Springdoc with Spring WebMvc.fn
To work with Spring’s functional endpoint API, some refactoring is required so that Springdoc can infer the API contracts. Create a NoteService
and the following code.
// src/main/java/dev/mflash/guides/springdoc/NoteService.java
@Tag(name = "Note", description = "Endpoints for CRUD operations on notes")
public @Service class NoteService {
private final NoteRepository repository;
public NoteService(NoteRepository repository) {
this.repository = repository;
}
public List<Note> save(List<Note> notes) {
List<Note> savedNotes = new ArrayList<>();
.saveAll(notes).forEach(savedNotes::add);
repositoryreturn savedNotes;
}
public List<Note> findAll() {
List<Note> savedNotes = new ArrayList<>();
.findAll().forEach(savedNotes::add);
repositoryreturn savedNotes;
}
public List<Note> upload(Part csv) throws IOException {
List<Note> savedNotes = new ArrayList<>();
List<Note> notes = new BufferedReader(
new InputStreamReader(csv.getInputStream(), StandardCharsets.UTF_8)).lines()
Note::parseNote).collect(Collectors.toList());
.map(.saveAll(notes).forEach(savedNotes::add);
repositoryreturn savedNotes;
}
public boolean delete(@Parameter(in = ParameterIn.PATH) long id) {
.deleteById(id);
repositoryreturn true;
} }
Note that
- the
@Tag
annotation is now applied on the service - a Swagger-specific
@Parameter
annotation is used to specify thatid
is a path variable to thedelete
method
Refactor the controller using Spring’s functional API.
// src/main/java/dev/mflash/guides/springdoc/NoteController.java
public @Controller class NoteController {
private final NoteService service;
public NoteController(NoteService service) {
this.service = service;
}
public ServerResponse save(ServerRequest request) throws ServletException, IOException {
final List<Note> newNotes = request.body(new ParameterizedTypeReference<>() {});
return ServerResponse.ok().contentType(APPLICATION_JSON).body(service.save(newNotes));
}
public ServerResponse findAll(ServerRequest request) {
return ServerResponse.ok().contentType(APPLICATION_JSON).body(service.findAll());
}
public ServerResponse upload(ServerRequest request) throws IOException, ServletException {
Part csv = request.servletRequest().getPart("data");
return ServerResponse.ok().contentType(APPLICATION_JSON).body(service.upload(csv));
}
public ServerResponse delete(ServerRequest request) {
long id = Long.parseLong(request.pathVariable("id"));
return ServerResponse.ok().contentType(APPLICATION_JSON).body(service.delete(id));
}
@RouterOperations({
@RouterOperation(path = "/note", method = PUT, beanClass = NoteService.class, beanMethod = "save"),
@RouterOperation(path = "/note", method = GET, beanClass = NoteService.class, beanMethod = "findAll"),
@RouterOperation(path = "/note", method = POST,
operation = @Operation(
operationId = "multipart-upload",
requestBody = @RequestBody(required = true, description = "Upload a csv of notes"),
responses = @ApiResponse()
),beanClass = NoteService.class, beanMethod = "upload"),
@RouterOperation(path = "/note/{id}", method = DELETE, beanClass = NoteService.class, beanMethod = "delete")
})public @Bean RouterFunction<ServerResponse> routes() {
return route()
RequestPredicates.path("/note"),
.nest(-> builder.PUT("/", this::save)
builder .GET("/", this::findAll)
.POST("/", RequestPredicates.accept(MULTIPART_FORM_DATA), this::upload)
.DELETE("/{id}", this::delete).build())
.build();
} }
Note that the controller is annotated with @Controller
, instead of @RestController
(why? 🤔). The interesting part is at the router configuration method routes
.
Springdoc provides @RouterOperation
annotation for a single-route configuration and @RouterOperations
annotation for multiple-route configuration (which is the case for the above example). Note that the beanClass
and beanMethod
are necessary to allow Springdoc inspect NoteService
and resolve the API contracts. Furthermore, for a multipart upload, we need to specify an @Operation
with a unique id and provide a customization to let Springdoc know that it is a multipart upload operation. This can be done by injecting an OpenApiCustomiser
bean as follows.
// src/main/java/dev/mflash/guides/springdoc/Launcher.java
public @SpringBootApplication class Launcher {
public static void main(String[] args) {
SpringApplication.run(Launcher.class, args);
}
public @Bean OpenAPI noteAPI() {
return new OpenAPI()
.info(new Info()
"Note API")
.title("A CRUD API to demonstrate Springdoc integration")
.description("0.0.1-SNAPSHOT")
.version(
.license(new License().name("MIT").url("https://opensource.org/licenses/MIT")
)
);
}
public @Bean OpenApiCustomiser openApiCustomiser() {
return openApi -> openApi.getPaths()
.stream().flatMap(pathItem -> pathItem.readOperations().stream())
.values()-> {
.forEach(operation if ("multipart-upload".equals(operation.getOperationId())) {
.getRequestBody()
operation
.setContent(new Content().addMediaType(
MediaType.MULTIPART_FORM_DATA_VALUE,
new io.swagger.v3.oas.models.media.MediaType()
new ObjectSchema().addProperties("data", new FileSchema()))
.schema(
)
);
}
});
} }
Once again, launch the application and open http://localhost:8080/swagger-ui.html to access the Swagger UI.
Source code
Related