Self documenting API with swagger (OpenAPI 3.0) documentation
Overview
API documentation is one of the crucial point during development of the application. The contracted API let development teams to work on different pieces of application, without risk of failure during integration. API contract also defines endpoint interaction results, such as status codes and business reasons for them.
Design first vs Code First approach
In case of designing software, we need to choose one of the API design approach.
Design first: | Contract -> Code the API |
---|
Design first approach:
- Parallelize development work - Teams can mock API, based on the expected behaviour provided in contract
- Single point of truth - The API contract is available within organization on early stage of the project.
Code first: | Code the API -> Contract / Documentation |
---|
Code first approach:
- Flexibility with delivering API
- Quick prototyping
Design first | Code First |
---|---|
Public API | Internal API |
Multiple teams | Single team |
Consuming design time | Quick prototyping |
Swagger
To simplify the process of building API documentation, tools such as Swagger has been developed.
Swagger toolset offers:
- OpenAPI (Swagger) documentation online editor
- Client/Server code generation based on written OpenAPI documentation
- Swagger UI
- Basic web visualization of the API, with possibility of testing endpoints
- Could be deployed on top of spring-boot application (with springdoc or springfox libraries)
- Swagger documentation could be automatically generated based on implemented API code.
As you can see different swagger tools can be used for both API-first and Code First approaches. In following article I would like to focus on code first approach and usage of SpringDoc library.
The example of deployed swagger documentation, could be found under following link: https://spring-link-shortener.herokuapp.com/swagger-ui/index.html
SpringDoc library
SpringDoc automatically generates documentation in JSON/YAML format and also presents it in Swagger UI. SpringDoc works by examining an application at runtime to infer API semantics based on spring configurations, class structure and annotations.
Library supports:
- OpenAPI 3
- Spring-boot (v1 and v2)
- JSR-303 (javax bean validation)
- Different types of authentication like: OAuth 2
- Webflux
Add library to project
The only step needed to add swagger documentation into your project is adding following dependency to pom.xml
:
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.6.7</version>
</dependency>
Then, if you run spring-boot project, the Swagger UI page (http://localhost:8080/swagger-ui/index.html) and documentation (http://localhost:8080/v3/api-docs) in json format should be exposed.
Result in Swagger UI:
Annotations
One of the most popular way to customize OpenAPI definitions is usage of annotations over specific fields, class or methods related to API.
@OpenAPIDefinition
- This is the root document object of the OpenAPI document. Contains fields such as: openapi and info and paths@Info
- Provides metadata about the API. Contains fields such as: title, description (in markdown), contact, license and version.
@Operation
- Describes a single API operation on a path. Wrapper annotation for request / responses and it’s parameters. Contains fields such as: summary and description.@ApiResponse
- Expected API response, could be used multiple times. Contains fields such as: description and responseCode and content@Schema
- Provides the definition of input and output data types. Contains fields such as: description, example, required.@Hidden
- Skipping a given resource, class or bean type during documentation generation.
Endpoint
Example controller:
import dev.greencashew.link_shortener.link.api.LinkService;
import dev.greencashew.link_shortener.link.api.dto.LinkDto;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@RestController
@AllArgsConstructor
@RequestMapping("/s")
class RedirectController {
private final LinkService service;
@GetMapping("/{id}")
@Operation(description = "Redirect link by it's identifier. This endpoint has to be tested by direct GET request in browser.")
@ApiResponse(responseCode = "302", description = "User is redirected to expected location.", content = @Content)
@ApiResponse(responseCode = "404", description = "Shortened link not found.", content = @Content(examples = @ExampleObject(value = "{\"errorMessage\": \"Shortened link link-alias not found.\"}")))
public void redirectLink(
@Schema(description = "Identifier/alias to link. Used for redirection.", example = "link-alias", required = true)
@PathVariable String id, HttpServletResponse httpServletResponse) throws IOException {
final LinkDto linkDto = service.gatherLinkAndIncrementVisits(id);
httpServletResponse.sendRedirect(linkDto.targetUrl());
}
}
Result in Swagger UI:
DTO
Example Dto object:
import io.swagger.v3.oas.annotations.media.Schema;
import javax.validation.constraints.Email;
import javax.validation.constraints.Future;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
import java.time.LocalDate;
record CreateLinkDto(
@Schema(description = "Identifier/alias to link. Used for redirection.", example = "link-alias", required = true)
@NotBlank @Size(min = 1, max = 60)
String id,
@Schema(description = "User email required for shortened link management (deletion, updating)", example = "test@greencashew.dev", required = true)
@NotBlank @Email
String email,
@Schema(description = "Destination url we would like to ", example = "https://github.com/greencashew/warsztaty-podstawy-springa", required = true)
@NotBlank
String targetUrl,
@Schema(description = "Link expiration time. If would like to have shortened link forever do not fill this field.", example = "2054-06-23", required = false)
@Future
LocalDate expirationDate) {
}
Result in Swagger UI:
Example value in documentation:
{
"id": "link-alias",
"email": "test@greencashew.dev",
"targetUrl": "https://github.com/greencashew/warsztaty-podstawy-springa",
"expirationDate": "2054-06-23"
}
OpenAPI Configuration
Another possibility of defining API documentation is usage bean configuration.
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.ExternalDocumentation;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
class DocumentationConfiguration {
@Bean
public OpenAPI springLinkShortenerDocumentation(@Value("${application.version}") String appVersion) {
return new OpenAPI()
.components(new Components().addSecuritySchemes("basicScheme", new SecurityScheme()
.type(SecurityScheme.Type.HTTP).scheme("basic")))
.info(new Info().title("Link Shortener")
.description("""
It is fully featured link shortener written in Java 17 and Spring Framework.
Supported features:
- Create/Read/Update/Delete shortened link
- Redirect to specific page by short link identifier
- Handle business exception like: LinkNotFound, LinkAlreadyExists or IncorrectAdminVerification
- Application automatically delete expired links within specified period
""")
.version(appVersion)
.contact(new Contact().name("Jan Górkiewicz").url("https://greencashew.dev"))
.license(new License().name("Apache 2.0")))
.externalDocs(new ExternalDocumentation()
.description("Project created as educational material of spring course 'Warsztaty Podstawy Springa'.")
.url("https://github.com/greencashew/warsztaty-podstawy-springa"));
}
}
Result in swagger UI:
Useful application properties
SpringDoc offers also configuration via application properties, below list of some useful parameters:
# Disabling the /v3/api-docs endpoint
springdoc.api-docs.enabled=false
# Disable swagger UI
springdoc.swagger-ui.enabled=false
# Change path to swagger UI
springdoc.swagger-ui.path=/swagger-ui.html
# Oauth Default clientSecret (only for dev/test environment)
springdoc.swagger-ui.oauth.clientSecret=6779ef20e75817b79602
springdoc.swagger-ui.oauth.appName=appName
# Show actuator endpoints
springdoc.show-actuator=true
# Specify packages to scan
springdoc.packagesToScan=package1, package2
#Use fully qualified names
springdoc.use-fqn=true
More information
More information could be found in official springdoc documentation and github demo applications repository.
Why not SpringFox library?
SpringFox library was most commonly used library for generating swagger (OpenAPI) documentation. It is still popular for OpenAPI 2. Unfortunately it is not actively maintained by its developers – the latest version has been released in July 2020. Springfox library integration with the latest spring boot version also require writing some workaround code due to issue in library. Therefore, it is good idea to use SpringDoc as replacement to SpringFox.
Issue new spring boot version and SpringFox library
Spring Boot Version: 2.6.x
SpringFox swagger: 3.0.0
org.springframework.context.ApplicationContextException: Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerException: Cannot invoke "org.springframework.web.servlet.mvc.condition.PatternsRequestCondition.getPatterns()" because "this.condition" is null
at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:181) ~[spring-context-5.3.16.jar:5.3.16]
at org.springframework.context.support.DefaultLifecycleProcessor.access$200(DefaultLifecycleProcessor.java:54) ~[spring-context-5.3.16.jar:5.3.16]
at org.springframework.context.support.DefaultLifecycleProcessor$LifecycleGroup.start(DefaultLifecycleProcessor.java:356) ~[spring-context-5.3.16.jar:5.3.16]
at java.base/java.lang.Iterable.forEach(Iterable.java:75) ~[na:na]
at org.springframework.context.support.DefaultLifecycleProcessor.startBeans(DefaultLifecycleProcessor.java:155) ~[spring-context-5.3.16.jar:5.3.16]
at org.springframework.context.support.DefaultLifecycleProcessor.onRefresh(DefaultLifecycleProcessor.java:123) ~[spring-context-5.3.16.jar:5.3.16]
at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:935) ~[spring-context-5.3.16.jar:5.3.16]
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:586) ~[spring-context-5.3.16.jar:5.3.16]
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:145) ~[spring-boot-2.6.4.jar:2.6.4]
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:740) ~[spring-boot-2.6.4.jar:2.6.4]
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:415) ~[spring-boot-2.6.4.jar:2.6.4]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:303) ~[spring-boot-2.6.4.jar:2.6.4]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1312) ~[spring-boot-2.6.4.jar:2.6.4]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1301) ~[spring-boot-2.6.4.jar:2.6.4]
at com.greencashew.springswagger.SpringSwaggerFixedApplication.main(SpringSwaggerFixedApplication.java:10) ~[classes/:na]
Caused by: java.lang.NullPointerException: Cannot invoke "org.springframework.web.servlet.mvc.condition.PatternsRequestCondition.getPatterns()" because "this.condition" is null
at springfox.documentation.spring.web.WebMvcPatternsRequestConditionWrapper.getPatterns(WebMvcPatternsRequestConditionWrapper.java:56) ~[springfox-spring-webmvc-3.0.0.jar:3.0.0]
at springfox.documentation.RequestHandler.sortedPaths(RequestHandler.java:113) ~[springfox-core-3.0.0.jar:3.0.0]
at springfox.documentation.spi.service.contexts.Orderings.lambda$byPatternsCondition$3(Orderings.java:89) ~[springfox-spi-3.0.0.jar:3.0.0]
at java.base/java.util.Comparator.lambda$comparing$77a9974f$1(Comparator.java:473) ~[na:na]
at java.base/java.util.TimSort.countRunAndMakeAscending(TimSort.java:355) ~[na:na]
at java.base/java.util.TimSort.sort(TimSort.java:220) ~[na:na]
at java.base/java.util.Arrays.sort(Arrays.java:1307) ~[na:na]
at java.base/java.util.ArrayList.sort(ArrayList.java:1721) ~[na:na]
at java.base/java.util.stream.SortedOps$RefSortingSink.end(SortedOps.java:392) ~[na:na]
at java.base/java.util.stream.Sink$ChainedReference.end(Sink.java:258) ~[na:na]
at java.base/java.util.stream.Sink$ChainedReference.end(Sink.java:258) ~[na:na]
at java.base/java.util.stream.Sink$ChainedReference.end(Sink.java:258) ~[na:na]
at java.base/java.util.stream.Sink$ChainedReference.end(Sink.java:258) ~[na:na]
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:510) ~[na:na]
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499) ~[na:na]
at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921) ~[na:na]
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) ~[na:na]
at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:682) ~[na:na]
at springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider.requestHandlers(WebMvcRequestHandlerProvider.java:81) ~[springfox-spring-webmvc-3.0.0.jar:3.0.0]
at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197) ~[na:na]
at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1625) ~[na:na]
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509) ~[na:na]
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499) ~[na:na]
at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921) ~[na:na]
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) ~[na:na]
at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:682) ~[na:na]
at springfox.documentation.spring.web.plugins.AbstractDocumentationPluginsBootstrapper.withDefaults(AbstractDocumentationPluginsBootstrapper.java:107) ~[springfox-spring-web-3.0.0.jar:3.0.0]
at springfox.documentation.spring.web.plugins.AbstractDocumentationPluginsBootstrapper.buildContext(AbstractDocumentationPluginsBootstrapper.java:91) ~[springfox-spring-web-3.0.0.jar:3.0.0]
at springfox.documentation.spring.web.plugins.AbstractDocumentationPluginsBootstrapper.bootstrapDocumentationPlugins(AbstractDocumentationPluginsBootstrapper.java:82) ~[springfox-spring-web-3.0.0.jar:3.0.0]
at springfox.documentation.spring.web.plugins.DocumentationPluginsBootstrapper.start(DocumentationPluginsBootstrapper.java:100) ~[springfox-spring-web-3.0.0.jar:3.0.0]
at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:178) ~[spring-context-5.3.16.jar:5.3.16]
... 14 common frames omitted
Solution for the issue could be found here.
Migrating from Springfox to SpringDoc
To migrate from SpringFox to SpringDoc change dependencies in pom.xml could be not enough. Another step is annotations migration from OpenAPI 2 to OpenAPI 3:
@Api
→@Tag
@ApiIgnore
→@Hidden
@ApiImplicitParam
→@Parameter
@ApiImplicitParams
→@Parameters
@ApiModel
→@Schema
@ApiModelProperty(hidden = true)
→@Schema(accessMode = READ_ONLY)
@ApiModelProperty
→@Schema
@ApiOperation(value = "foo", notes = "bar")
→@Operation(summary = "foo", description = "bar")
@ApiParam
→@Parameter
@ApiResponse(code = 404, message = "foo")
→@ApiResponse(responseCode = "404", description = "foo")
More information
More information about springfox project could be found in official springfox documentation.
Tags: Spring Boot Swagger Springdoc
Maybe you want to share? :)