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 firstCode First
Public APIInternal API
Multiple teamsSingle team
Consuming design timeQuick prototyping

Swagger

To simplify the process of building API documentation, tools such as Swagger has been developed.

Swagger

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: Added springdoc library

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:

Documented rest endpoint

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: Request body with schema

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: Swagger documentation configuration.png

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.