Documenting RESTful APIs in Spring with Open API spec

Intro

Open API (previously Swagger) is world’s most widely used API description standard and it provides a formal specification for describing HTTP APIs.

This allows people to understand how and API works and how a sequence of API’s work together.

Specification is machine-readable so we can generate client code, create tests and much, much more…​

Open API specification is widely used so enjoys support of wide range of vendors and tools.

History

Swagger came up with proposal for specification for describing RESTful APIs.

The specification creates a RESTful interface for easily developing and consuming an API by effectively mapping all the resources and operations associated with it.

Swagger spec became adopted by the industry and became industry standard know as OPEN API specification.

Spec had a couple of guidelines it was created by:

  • Spec should be written in JSON or YAML format.

  • It would be used to describe RESTful APIs

  • The goal is to keep the spec machine-readable

  • But also make it easy for humans to understand as well

Swagger also created Swagger-ui - popular web user interface for displaying and interacting with endpoints.

image$swagger io

Key moments in history of the spec:

  • 2010 - Swagger

  • 2014 - Open API 2.0

  • 2017 - Open API 3.0

  • 2019 - springdoc-openapi

  • 2021 - Open API 3.1

Spring doc - Open API

springdoc-openapi is library that allows automatic generation of open API documentation for your Spring projects.

It provides swagger user interface with a list of your endpoints and the ability to generate open API json spec.

Library is available as a dependency depending on whether you are using webMvc or webflux frameworks, and secondly if you need only open API spec generation, or swagger UI as well.

image$springdoc openapi

In our project case, where we use webMvc framework, we want the ability to generate both generate api spec and display swagger user interface, so we’ve included webmvc-ui starter.

<properties>
    <springdoc.version>2.7.0</springdoc.version>
</properties>
<dependency>
	<groupId>org.springdoc</groupId>
	<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
	<!--only open api spec, no swagger UI-->
	<!--<artifactId>springdoc-openapi-starter-webmvc-api</artifactId>-->
	<version>${springdoc.version}</version>
</dependency>

Out of the box integration

Out of the box Spring Doc - Open API library is integrated with the rest of the Spring ecosystem and with just inclusion of library as dependency on our project we’ll get out of the box rich functionality.

To demonstrate library features we’ve created a project that has following starters:

webMvc

web application support, including REST, using Spring MVC

actuator

built in (or custom) endpoints that let you monitor and manage your application - such as application health, metrics, sessions, etc.

Spring Data JPA

persistence support with Java Persistence API

validations

bean validation support

We can tell springdoc-openapi library to scan packages or include paths from our application in open API spec in two ways.

Either by specifying properties in our application.properties:

# Packages to scan
springdoc.packagesToScan=com.package1, com.package2
# Paths to include
springdoc.pathsToMatch=/v1, /api/balance/**

or programmatically by defining GroupedOpenApi bean.

@Bean
public GroupedOpenApi publicApi() {
  return GroupedOpenApi.builder()
          .group("springshop-public")
          .pathsToMatch("/public/**")
          .build();
}

Spring @RestControllers

We’ll get our api definition by just using normal Spring webMvc annotations and marking our package to be scanned by springdoc-openapi library.

For example class definition:

@RestController
@RequestMapping("/api/v1/email")
public class EmailApiController implements EmailApi {

    @Override
    @PostMapping
    public ResponseEntity<EmailResponse> sendEmail(@RequestBody @Valid EmailRequest emailRequest) {
        return ResponseEntity.ok(new EmailResponse("message-id", "Queued. Thank you."));
    }

    @Override
    @GetMapping("/{id}")
    public ResponseEntity<String> checkStatus(@PathVariable String id) {
        return ResponseEntity.ok(id);
    }

    @GetMapping("/filter")
    public Page<Object> filterEmails(Pageable pageable) {
        throw new RuntimeException("TODO");
    }
}

will be reflected in swagger ui:

image$spring rest controller openapi

Spring @RestControllerAdvice

Possible non success http responses are displayed automatically by using Springs @RestControllerAdvice and annotating our exception handler methods with appropriate response statuses.

For example, exception handlers for BAD_REQUEST (400) and INTERNAL_SERVER_ERROR (500):

@ExceptionHandler(exception = BindException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
ResponseEntity<EmailApi.EmailError> processValidationException(WebRequest request) {
    String bidingErrors = ((BindException) errorAttributes.getError(request))
            .getBindingResult()
            .getAllErrors()
            .stream()
            .map(oe -> (oe instanceof FieldError fe)
                    ? fe.getField() + ": " + fe.getDefaultMessage()
                    : oe.getDefaultMessage()
            )
            .collect(Collectors.joining(", "));

    return ResponseEntity.status(HttpStatus.BAD_REQUEST)
            .body(new EmailApi.EmailError(bidingErrors));
}

@ExceptionHandler(RuntimeException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
ResponseEntity<EmailApi.EmailError> processServerErrors(RuntimeException e) {
    return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(new EmailApi.EmailError(e.getLocalizedMessage()));
}

are picked up as possible responses in swagger ui api definition:

image$spring controller advice status swagger

Actuator REST API

Actuator provides built in (or custom) endpoints that let you monitor and manage your application - such as application health, metrics, sessions, etc.

It comes with list of endpoints that are by default available under

/actuator root directory.

By changing properties in our application.properties configuration, we’ll allow actuator endpoints to be displayed in our app open API spec. We’ll show all endpoints actuator has to offer, and change default root actuator url from /actuator to /management.

springdoc.show-actuator=true
management.endpoints.web.exposure.include=*
management.endpoints.web.base-path=/management

In our spring app, open api spec is, by default, available on: /v3/api-docs, and swagger ui on: /swagger-ui/index.html

image$swagger actuator group

Spring Data JPA

Some of the Spring Data JPA framework build in objects, like Page and Pageable are automatically available in schema definition.

For method definition:

Page<Object> filterEmails(@ParameterObject Pageable pageable);

we’d get automatic example and schema description in swagger ui:

image$spring data jpa schema

Validations

By including validation starter and validation annotation like @Size:

record EmailRequest(@Email @NotBlank String to, @Size(min = 3, max = 100) String subject,@NotBlank String text) {}

restrictions are reflected in swagger ui schema fields definition:

image$validation annotations

Open API spec plugin

If we’d like open API spec (openapi.json) to be available as part of our build process, we can include maven plugin that will generate it during verify phase on the root of our target directory.

	<executions>
		<execution>
			<goals>
				<goal>start</goal>
				<goal>stop</goal>
			</goals>
		</execution>
	</executions>
</plugin>
<plugin>
	<groupId>org.springdoc</groupId>
	<artifactId>springdoc-openapi-maven-plugin</artifactId>
	<version>1.4</version>
	<executions>
		<execution>
			<id>integration-test</id>
			<goals>
				<goal>generate</goal>
			</goals>
		</execution>
	</executions>
</plugin>

Groups

Groups lets us aggregate API under common name by specifying endpoint paths to match or packages to scan.

In this example we group actuator endpoints under one, and our own endpoints under another group:

@Value("${springdoc.version}")
String springDocVersion;

@Bean
GroupedOpenApi actuatorApi(WebEndpointProperties endpointProperties) {
    return GroupedOpenApi
            .builder()
            .addOpenApiCustomizer(apiCustomizer -> apiCustomizer.info(new Info().version(springDocVersion).description("Actuator API")))
            .group("actuator")
            .pathsToMatch(endpointProperties.getBasePath() + ALL_PATTERN)
            .build();
}

@Bean
GroupedOpenApi emailApi() {
    return GroupedOpenApi
            .builder()
            .addOpenApiCustomizer(apiCustomizer -> apiCustomizer.info(new Info().description("Email API")))
            .group("email")
            .packagesToScan("com.kanezi.springdoc_openapi_showcase")
            .build();
}

We noted our app root package com.kanezi.springdoc_openapi_showcase to be used in packagesToScan method which means that our app every controller rest api definition will be under email group.

Groups definitions are reflected in swagger ui group definition selection.

image$swagger ui group selection

Customization

To customize default behaviour of springdoc-openapi library and configure how our endpoints are displayed we can change application properties or configure behaviour programmatically.

If our application only has REST API on i.e. /api/v1/…​ and does not have default web ui, we can display swagger by default on root url by setting property:

# Opens swagger ui on base url
springdoc.swagger-ui.use-root-path=true

REST API definition customization

By annotating our class and method definitions with springdoc-openapi annotations, i.e.

  • @Operation

  • @ApiResponse

  • @RequestBody

  • …​

we can further customize how is our api definition described in open Api and swagger UI.

Especially useful is @RequestBody content part where we can use @ExampleObject to provide valid json example for our users to immediately test our api.

Example of method customization:

@Operation(summary = "sends email", tags = {"email"}
        , description = """
        sends email <strong>to</strong> and email address
        <br>
        with <strong>subject</strong>
        <br>
        and <strong>text</strong>
        """)
@ApiResponses(value = {
        @ApiResponse(responseCode = "200", description = "email successfully send",
                content = @Content(schema = @Schema(implementation = EmailRequest.class)
                        , examples = @ExampleObject(value = """
                        {
                            "id": "message-id",
                            "message": "Queued. Thank you."
                        }
                        """)))
})
ResponseEntity<EmailResponse> sendEmail(
        @RequestBody(
                description = "email message",
                content = @Content(examples = @ExampleObject(value = """
                        {
                            "to": "user@example.com",
                            "subject": "test mail",
                            "text": "Welcome to our site!"
                        }
                        """)))
        EmailRequest emailRequest);
image$swagger ui ExampleObject example