Documenting Spring Boot REST APIs with OpenAPI and springdoc
Introduction
This tutorial documents a Spring Boot REST API with OpenAPI, using the springdoc-openapi library to generate the documentation from the code and serve it, together with the interactive Swagger UI. We will start with what OpenAPI is and why generating the description from code beats writing it by hand. We will then add springdoc to a book API and read the OpenAPI document it produces with no annotations at all. Next we will enrich it with annotations - tags, operation summaries, response codes, and schema descriptions and examples - plus general information about the API, and read the improved document. Lastly, we will look at Swagger UI, the configuration properties that matter, and how to keep the documentation out of production when needed.(This tutorial documents the kind of API built in this module, with the validation from Bean Validation in Spring BootClick to read tutorial: Bean Validation in Spring Boot: @Valid and Custom Validators and the problem responses from REST Error Handling with @ControllerAdvice and ProblemDetailClick to read tutorial: REST Error Handling with @ControllerAdvice and ProblemDetail.)
What is OpenAPI?
The OpenAPI Specification (formerly known as Swagger) is a standard, language-neutral format for describing an HTTP API in JSON or YAML: its endpoints, their parameters and request bodies, the possible responses with their status codes, and the structure of every JSON object. One OpenAPI document serves many purposes -- Documentation that people read in a browser, with tools such as Swagger UI or Redoc.
- Client generation: tools such as OpenAPI Generator create client libraries in Java, TypeScript, Python and many other languages.
- Testing and mocking: tools can send requests based on it, or simulate the API before it exists.
- API gateways can import it to set up routing and validation.
Adding springdoc
One dependency adds both the OpenAPI document and Swagger UI to a Spring MVC application. Version 3 of springdoc supports Spring Boot 4; 3.2.0 is built against Spring Boot 4.1.2 -pom.xml - springdoc-openapi for Spring MVC
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>3.2.0</version>
</dependency>
The API to document
The book API below combines ordinary Spring MVC code with OpenAPI annotations from theio.swagger.v3.oas.annotations package - @Tag, @Operation, @ApiResponse, @Parameter and @Schema - and an OpenAPI bean with general information about the API -
DocumentedApiApp.java - a book API annotated for OpenAPI
//DocumentedApiApp.java
package com.javabrahman.rest.openapi;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Positive;
import java.math.BigDecimal;
import java.util.List;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@Schema(description = "A book in the catalogue")
record Book(
@Schema(description = "Catalogue id, assigned by the server", example = "1") long id,
@Schema(description = "Title of the book", example = "Effective Java") String title,
@Schema(description = "Price in US dollars", example = "45.99") BigDecimal price) {
}
@Schema(description = "The data needed to add a book")
record NewBook(
@NotBlank @Schema(example = "Clean Code") String title,
@Positive @Schema(example = "37.49") BigDecimal price) {
}
class BookNotFoundException extends RuntimeException {
BookNotFoundException(long id) {
super("No book with id " + id);
}
}
@RestController
@RequestMapping("/api/books")
@Tag(name = "Books", description = "Browse and add books in the catalogue")
class BookController {
@GetMapping
@Operation(summary = "List all books")
List<Book> all() {
return List.of(new Book(1, "Effective Java", new BigDecimal("45.99")));
}
@GetMapping("/{id}")
@Operation(summary = "Get one book")
@ApiResponse(responseCode = "200", description = "The book")
@ApiResponse(responseCode = "404", description = "No book with this id",
content = @Content(mediaType = "application/problem+json", schema = @Schema(implementation = ProblemDetail.class)))
Book one(@Parameter(description = "Catalogue id of the book", example = "1") @PathVariable long id) {
if (id != 1) {
throw new BookNotFoundException(id);
}
return all().getFirst();
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
@Operation(summary = "Add a book", description = "Adds a book and returns it with its new id.")
Book create(@Valid @RequestBody NewBook book) {
return new Book(2, book.title(), book.price());
}
}
@RestControllerAdvice
class ApiExceptionHandler {
@ExceptionHandler(BookNotFoundException.class)
ProblemDetail notFound(BookNotFoundException e) {
return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, e.getMessage());
}
}
@SpringBootApplication
public class DocumentedApiApp {
@Bean
OpenAPI apiInfo() {
return new OpenAPI().info(new Info()
.title("JavaBrahman Book Catalogue API")
.version("1.0")
.description("A small example API for the Spring REST tutorials.")
.contact(new Contact().name("JavaBrahman").url("https://www.javabrahman.com")));
}
public static void main(String[] args) {
SpringApplication.run(DocumentedApiApp.class, args);
}
}
The generated OpenAPI document
springdoc serves the document at/v3/api-docs as JSON and at /v3/api-docs.yaml as YAML. Fetching the YAML version from the running application -
OUTPUT of GET /v3/api-docs.yaml> curl -i http://localhost:8080/v3/api-docs.yaml
HTTP/1.1 200
Content-Length: 3038
Content-Type: application/vnd.oai.openapi
openapi: 3.1.0
info:
title: JavaBrahman Book Catalogue API
description: A small example API for the Spring REST tutorials.
contact:
name: JavaBrahman
url: https://www.javabrahman.com
version: "1.0"
servers:
- url: http://localhost:8080
description: Generated server url
tags:
- name: Books
description: Browse and add books in the catalogue
paths:
/api/books:
get:
tags:
- Books
summary: List all books
operationId: all
responses:
"200":
description: OK
content:
'*/*':
schema:
type: array
items:
$ref: "#/components/schemas/Book"
post:
tags:
- Books
summary: Add a book
description: Adds a book and returns it with its new id.
operationId: create
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/NewBook"
required: true
responses:
"201":
description: Created
content:
'*/*':
schema:
$ref: "#/components/schemas/Book"
/api/books/{id}:
get:
tags:
- Books
summary: Get one book
operationId: one
parameters:
- name: id
in: path
description: Catalogue id of the book
required: true
schema:
type: integer
format: int64
example: 1
responses:
"200":
description: The book
content:
'*/*':
schema:
$ref: "#/components/schemas/Book"
"404":
description: No book with this id
content:
application/problem+json:
schema:
$ref: "#/components/schemas/ProblemDetail"
components:
schemas:
NewBook:
type: object
description: The data needed to add a book
properties:
title:
type: string
example: Clean Code
minLength: 1
price:
type: number
example: 37.49
exclusiveMinimum: 0
required:
- title
Book:
type: object
description: A book in the catalogue
properties:
id:
type: integer
format: int64
description: "Catalogue id, assigned by the server"
example: 1
title:
type: string
description: Title of the book
example: Effective Java
price:
type: number
description: Price in US dollars
example: 45.99
ProblemDetail:
type: object
properties:
type:
type: string
format: uri
title:
type: string
status:
type: integer
format: int32
detail:
type: string
instance:
type: string
format: uri
properties:
type: object
additionalProperties: {}
Explanation of the document
openapigives the version of the specification, andinfocomes from theOpenAPIbean: the title, version, description and contact.serverslists the URL the document was requested from, so tools know where to send requests.tagsgroups operations;Booksand its description come from@Tagon the controller.- Under
paths, every mapping appears with its HTTP method, anoperationIdtaken from the method name, thesummaryanddescriptionfrom@Operation, its parameters - with theidpath parameter's description and example from@Parameter- and its request body and responses. - The
201response of thePOSTwas inferred from@ResponseStatus(HttpStatus.CREATED), and the404of the single-book endpoint, with itsapplication/problem+jsonbody, came from@ApiResponse. - Under
components/schemas, each record became a schema, with property types, descriptions and examples from@Schema. The validation annotations were translated too:@NotBlankmadetitlerequired with aminLength, and@Positivebecame anexclusiveMinimumof 0.ProblemDetailgot a schema as well. - Much of the document came from the Spring code alone: the paths and methods from the mappings, parameter and property types from the Java types, the request body from
@RequestBody, the201from@ResponseStatus, and the constraints from the validation annotations. Theio.swaggerannotations added the human-friendly parts - descriptions, examples - and the404response, which springdoc cannot infer from an exception thrown inside the method.
Swagger UI
springdoc also serves Swagger UI, an interactive web page that renders the OpenAPI document: every endpoint grouped by tag, with its parameters, schemas and examples, and a Try it out button that sends real requests to the API from the browser. It is at/swagger-ui.html, which redirects to the page itself -
OUTPUT of requesting /swagger-ui.html> curl -i http://localhost:8080/swagger-ui.html HTTP/1.1 302 Location: /swagger-ui/index.html Content-Length: 0
302 response points the browser to /swagger-ui/index.html, the Swagger UI page, which loads the OpenAPI document from /v3/api-docs and displays it. Opening http://localhost:8080/swagger-ui.html in a browser while the application runs shows the book API, ready to be tried out.
Useful springdoc properties
springdoc is configured withspringdoc.* properties in application.properties -
springdoc.api-docs.path=/api-docsandspringdoc.swagger-ui.path=/docschange where the document and the UI are served.springdoc.packages-to-scanandspringdoc.paths-to-match=/api/**limit the document to some controllers or paths - useful to keep internal endpoints out.springdoc.swagger-ui.operations-sorter=methodandtags-sorter=alphacontrol the order in the UI.springdoc.api-docs.enabled=falseandspringdoc.swagger-ui.enabled=falseswitch them off - typically in a production profile, if the API is not meant to be explored publicly.
Good practices
- Let the code be the source of truth: rely on springdoc's inference, and add annotations only where they add information - summaries, examples, extra responses.
- Document error responses, in particular the problem types your API returns, so that client developers know what to handle.
- Publish the document as part of the build, for example by saving
/v3/api-docsin a pipeline step, so that changes to the API show up in code review and clients can be generated from a fixed version. - Decide deliberately whether production exposes the document and Swagger UI; if it does, secure them like any other endpoint (Spring Security is covered in module 5).
- Version the document along with the API, as described in API Versioning in Spring Framework 7Click to read tutorial: API Versioning in Spring Framework 7 and Spring Boot 4.
Summary
In this tutorial we first saw what the OpenAPI Specification is, what an OpenAPI document is used for, and why springdoc generates it from the code. We then addedspringdoc-openapi-starter-webmvc-ui 3.2.0 to a book API, and annotated it with @Tag, @Operation, @ApiResponse, @Parameter and @Schema, with general information from an OpenAPI bean. Next we read the generated YAML document - paths, responses, and schemas that even include the validation rules - and saw how Swagger UI is reached. Lastly, we listed the most useful springdoc properties and good practices. The final tutorial of this module covers CORS, content negotiation and file uploads: CORS, Content Negotiation and File UploadsClick to read tutorial: CORS, Content Negotiation and File Uploads in Spring Boot.Spring REST API Tutorials
Calling REST Services with RestClient and HTTP InterfacesClick to read tutorial: Calling REST Services with RestClient and HTTP Interfaces CORS, Content Negotiation and File Uploads in Spring BootClick to read tutorial: CORS, Content Negotiation and File Uploads in Spring Boot Building a REST API with Spring Boot and @RestControllerClick to read tutorial: Building a REST API with Spring Boot and @RestController Spring MVC Request Mapping: Paths, Params and BodiesClick to read tutorial: Spring MVC Request Mapping: Paths, Params and Bodies ResponseEntity and HTTP Status Codes in Spring BootClick to read tutorial: ResponseEntity and HTTP Status Codes in Spring Boot Bean Validation in Spring Boot: @Valid and Custom ValidatorsClick to read tutorial: Bean Validation in Spring Boot: @Valid and Custom Validators REST Error Handling with @ControllerAdvice and ProblemDetailClick to read tutorial: REST Error Handling with @ControllerAdvice and ProblemDetail
Calling REST Services with RestClient and HTTP InterfacesClick to read tutorial: Calling REST Services with RestClient and HTTP Interfaces CORS, Content Negotiation and File Uploads in Spring BootClick to read tutorial: CORS, Content Negotiation and File Uploads in Spring Boot Building a REST API with Spring Boot and @RestControllerClick to read tutorial: Building a REST API with Spring Boot and @RestController Spring MVC Request Mapping: Paths, Params and BodiesClick to read tutorial: Spring MVC Request Mapping: Paths, Params and Bodies ResponseEntity and HTTP Status Codes in Spring BootClick to read tutorial: ResponseEntity and HTTP Status Codes in Spring Boot Bean Validation in Spring Boot: @Valid and Custom ValidatorsClick to read tutorial: Bean Validation in Spring Boot: @Valid and Custom Validators REST Error Handling with @ControllerAdvice and ProblemDetailClick to read tutorial: REST Error Handling with @ControllerAdvice and ProblemDetail