Swagger API Documentation Example in Spring Boot 4

Swagger API documentation is a web page, Swagger UI, generated from an OpenAPI document. In Spring Boot 4 we add it with one springdoc-openapi dependency and describe the endpoints with annotations such as @Operation and @Schema.

Final Swagger2 REST API Output

Swagger API documentation is a web page, called Swagger UI, that lists every endpoint of a REST API and lets us send test requests to the endpoints from the browser. Swagger UI builds the page from an OpenAPI document, which is a JSON or YAML file that describes the paths, parameters, request bodies and responses of the API. In a Spring Boot application, the springdoc-openapi library generates both from our controller code.

We use Swagger documentation to share the API contract with frontend developers and other teams, and to call an endpoint during development without writing a curl command first.

The following example adds Swagger documentation to a Spring Boot 4 app. We add one dependency, and the two URLs in the comments start working when the app runs.

<dependency>
  <groupId>org.springdoc</groupId>
  <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
  <version>3.1.1</version>
</dependency>

<!-- Swagger UI:        http://localhost:8080/swagger-ui.html -->
<!-- OpenAPI document:  http://localhost:8080/v3/api-docs     -->

Notice that we write no configuration class and no annotation. The library finds our @RestController classes and documents their request mappings with the default settings.

Next, we see how Swagger and OpenAPI relate and build a small recipes API. After that, we describe the API with annotations such as @Operation and @Schema, change the default URLs, turn the docs off in production and migrate old Springfox code.

1. What Is Swagger and OpenAPI?

The OpenAPI Specification (formerly the Swagger Specification) is a standard format for describing REST APIs in JSON or YAML. Swagger is the name of a set of open-source tools built around the specification, such as Swagger UI and Swagger Editor. So when developers say “Swagger docs”, they mean an OpenAPI document that Swagger UI shows as a web page.

Four names show up in Swagger tutorials, and each one is a different thing.

NameWhat it isIn our example
OpenAPI SpecificationThe format. Version 2.0 is also called Swagger 2.0; the current versions are 3.0 and 3.1.The document at /v3/api-docs, version 3.1.0
Swagger UIA web page that renders an OpenAPI document and sends test requests/swagger-ui.html
springdoc-openapiA Java library that generates the OpenAPI document from Spring MVC or Spring WebFlux code and bundles Swagger UIThe dependency in pom.xml
SpringfoxAn older library that generated Swagger 2.0 documents. Its last release is 3.0.0 from July 2020.Not used

For example, a mobile team builds the app for a recipes website, and our team writes the REST API behind it. A wiki page with the endpoints is out of date after the next release. Swagger UI is generated from the running code, so the mobile developers always see the endpoints of the deployed version.

The diagram shows the flow from our code to Swagger UI. The springdoc-openapi library reads our request mappings and annotations when the first request for the docs arrives, and serves the result as an OpenAPI document. Swagger UI loads that document and renders it.

Flow diagram. Our code, RecipeController with @GetMapping, @Operation and @ApiResponse, and the record Recipe with @Schema and @NotBlank, goes to springdoc-openapi, which reads the request mappings, parameter and return types, validation constraints and Swagger annotations. springdoc-openapi produces /v3/api-docs, an OpenAPI 3.1 document in JSON, or /v3/api-docs.yaml for YAML. Swagger UI at /swagger-ui.html loads the document and renders it as a page, and its Try it out button sends real HTTP requests to the endpoints of the running app at /api/recipes.
We write controllers and annotations. springdoc-openapi turns them into the OpenAPI document, and Swagger UI renders the document.

2. Swagger Documentation Example With Spring Boot

The following example is a small REST API for recipes. A Recipe has an id, a name and a cooking time in minutes. The RecipeController reads and creates recipes over HTTP. The app runs on Spring Boot 4.1.1 (Spring Framework 7.0.9) and Java 25, with springdoc-openapi 3.1.1, which bundles Swagger UI 5.32.14. The complete project is in the spring-webmvc repository on GitHub.

2.1. Adding the springdoc-openapi Dependency

For a Spring MVC app, we add the springdoc-openapi-starter-webmvc-ui dependency from the intro. It brings the OpenAPI generator and the Swagger annotations, and it includes the Swagger UI files. The library has other starters for other cases.

  • The starter springdoc-openapi-starter-webflux-ui does the same for Spring WebFlux apps.
  • The starter springdoc-openapi-starter-webmvc-api generates only the OpenAPI document, without Swagger UI.

The major version of springdoc-openapi must match the major version of Spring Boot, because springdoc-openapi releases a new major version together with each Spring Boot major release.

Spring Bootspringdoc-openapiStarter for Spring MVC
4.x3.xspringdoc-openapi-starter-webmvc-ui
3.x2.xspringdoc-openapi-starter-webmvc-ui
2.x1.xspringdoc-openapi-ui

With springdoc-openapi 2.x, a Spring Boot 4 app does not start, as we will see in section 7.1.

2.2. A REST Controller Without Swagger Annotations

We start with a controller that has no Swagger annotations at all. The Recipe class is a Java record, and the controller keeps the recipes in a ConcurrentHashMap. The @GetMapping and @PostMapping annotations map the HTTP requests.

public record Recipe(Long id, String name, int minutes) {
}

@RestController
@RequestMapping("/api/recipes")
public class RecipeController {

  private final Map<Long, Recipe> recipes = new ConcurrentHashMap<>();
  private final AtomicLong ids = new AtomicLong();

  @GetMapping
  public List<Recipe> findAll() {
    return List.copyOf(recipes.values());
  }

  @GetMapping("/{id}")
  public ResponseEntity<Recipe> findById(@PathVariable Long id) {
    Recipe recipe = recipes.get(id);
    return recipe == null ? ResponseEntity.notFound().build() : ResponseEntity.ok(recipe);
  }

  @PostMapping
  public ResponseEntity<Recipe> create(@RequestBody Recipe request) {
    long id = ids.incrementAndGet();
    Recipe saved = new Recipe(id, request.name(), request.minutes());
    recipes.put(id, saved);
    return ResponseEntity.created(URI.create("/api/recipes/" + id)).body(saved);
  }
}

When we open http://localhost:8080/v3/api-docs, springdoc-openapi returns the OpenAPI document for the three operations. The shortened output shows the POST operation and the Recipe schema.

{
  "openapi": "3.1.0",
  "info": {
    "title": "OpenAPI definition",
    "version": "v0"
  },
  "paths": {
    "/api/recipes": {
      "post": {
        "tags": ["recipe-controller"],
        "operationId": "create",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/Recipe" }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "*/*": {
                "schema": { "$ref": "#/components/schemas/Recipe" }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Recipe": {
        "type": "object",
        "properties": {
          "id": { "type": "integer", "format": "int64" },
          "name": { "type": "string" },
          "minutes": { "type": "integer", "format": "int32" }
        }
      }
    }
  }
}

We can see that the paths, the request body and the field types are correct. But the document has gaps that confuse anyone who reads the docs.

  • The title is “OpenAPI definition”, and the tag is the class name recipe-controller.
  • The POST operation says it returns 200, but the method returns 201 Created.
  • The 404 response of findById() is missing, because springdoc-openapi cannot see it in the method signature.
  • The response media type is \/\**, because the mapping does not declare what it produces.

We fix these gaps with annotations in section 3.

2.3. Opening Swagger UI and the OpenAPI Document

By default, springdoc-openapi serves the docs at three URLs.

URLContent
/v3/api-docsThe OpenAPI document in JSON
/v3/api-docs.yamlThe same document in YAML
/swagger-ui.htmlSwagger UI. The URL redirects (HTTP 302) to /swagger-ui/index.html.

The app also logs two warnings at startup, because both endpoints are enabled by default. We turn them off for production in section 5.2.

WARN  o.s.core.events.SpringDocAppInitializer  : SpringDoc /v3/api-docs endpoint is enabled by default. To disable it in production, set the property 'springdoc.api-docs.enabled=false'
WARN  o.s.core.events.SpringDocAppInitializer  : SpringDoc /swagger-ui.html endpoint is enabled by default. To disable it in production, set the property 'springdoc.swagger-ui.enabled=false'

3. Describing the API With Swagger Annotations

The Swagger annotations add the information that springdoc-openapi cannot read from the code, such as a summary of each operation or the meaning of each status code. They come from the package io.swagger.v3.oas.annotations of the swagger-core library, which the starter already includes. The annotations change only the OpenAPI document, so the behavior of the endpoints stays the same.

3.1. Grouping and Summaries With @Tag and @Operation

The @Tag annotation on the controller class puts all its operations into one group in Swagger UI, with a readable name instead of recipe-controller. The @Operation annotation on a method sets a one-line summary, which Swagger UI shows next to the path, and a longer description, which shows when we open the operation.

@Tag(name = "Recipes", description = "Create and read recipes")
@RestController
@RequestMapping(path = "/api/recipes", produces = MediaType.APPLICATION_JSON_VALUE)
public class RecipeController {

  @Operation(summary = "List all recipes")
  @GetMapping
  public List<Recipe> findAll() {
    return List.copyOf(recipes.values());
  }
}

We also added produces = MediaType.APPLICATION_JSON_VALUE to the class-level @RequestMapping, so the OpenAPI document lists application/json as the response media type of every operation instead of \/\**.

3.2. Documenting Status Codes With @ApiResponse

The @ApiResponse annotation documents one status code of an operation. We repeat the annotation for each code, so we don’t need the @ApiResponses wrapper. When a method has at least one @ApiResponse, springdoc-openapi documents only the listed status codes, so we list the success code as well.

@Operation(summary = "Find a recipe by its id")
@ApiResponse(responseCode = "200", description = "Recipe found")
@ApiResponse(responseCode = "404", description = "No recipe with this id", content = @Content)
@GetMapping("/{id}")
public ResponseEntity<Recipe> findById(
    @Parameter(description = "Recipe id", example = "1") @PathVariable Long id) {
  Recipe recipe = recipes.get(id);
  return recipe == null ? ResponseEntity.notFound().build() : ResponseEntity.ok(recipe);
}

@Operation(summary = "Create a recipe", description = "The server generates the id.")
@ApiResponse(responseCode = "201", description = "Recipe created")
@ApiResponse(responseCode = "400", description = "Invalid recipe", content = @Content)
@PostMapping
public ResponseEntity<Recipe> create(@Valid @RequestBody Recipe request) {
  Recipe saved = save(request);
  return ResponseEntity.created(URI.create("/api/recipes/" + saved.id())).body(saved);
}

The empty @Content on the 404 and 400 responses tells springdoc-openapi that these responses have no Recipe body. Without it, springdoc-openapi adds the method’s return type to every response. The generated responses of findById() look like this.

"responses": {
  "200": {
    "description": "Recipe found",
    "content": {
      "application/json": {
        "schema": { "$ref": "#/components/schemas/Recipe" }
      }
    }
  },
  "404": {
    "description": "No recipe with this id"
  }
}

3.3. Describing Parameters With @Parameter

The @Parameter annotation in section 3.2 describes the path variable id. The example value fills the input field in Swagger UI, so a tester can press Execute without typing an id. The same annotation works for query parameters (@RequestParam) and headers (@RequestHeader).

"parameters": [
  {
    "name": "id",
    "in": "path",
    "description": "Recipe id",
    "required": true,
    "schema": { "type": "integer", "format": "int64" },
    "example": 1
  }
]

3.4. Describing the Model With @Schema and Bean Validation

The @Schema annotation describes a model class or one of its fields. We put it on the record and on each record component. The library also reads the Bean Validation constraints, so @NotBlank, @Min and @Max turn into the matching OpenAPI rules without extra annotations. The constraints need the spring-boot-starter-validation dependency.

@Schema(description = "A recipe with its cooking time")
public record Recipe(

    @Schema(description = "Generated by the server", example = "1", accessMode = Schema.AccessMode.READ_ONLY)
    Long id,

    @Schema(description = "Recipe name", example = "Pancakes")
    @NotBlank
    String name,

    @Schema(description = "Cooking time in minutes", example = "20")
    @Min(1) @Max(600)
    int minutes) {
}

The generated schema shows each mapping. The @NotBlank constraint becomes required plus minLength: 1, the @Min and @Max constraints become minimum and maximum, and READ_ONLY marks the id as a value that the server sets.

"Recipe": {
  "type": "object",
  "description": "A recipe with its cooking time",
  "properties": {
    "id": {
      "type": "integer",
      "format": "int64",
      "description": "Generated by the server",
      "example": 1,
      "readOnly": true
    },
    "name": {
      "type": "string",
      "description": "Recipe name",
      "example": "Pancakes",
      "minLength": 1
    },
    "minutes": {
      "type": "integer",
      "format": "int32",
      "description": "Cooking time in minutes",
      "example": 20,
      "maximum": 600,
      "minimum": 1
    }
  },
  "required": ["name"]
}

Swagger has its own @RequestBody annotation in the package io.swagger.v3.oas.annotations.parameters. In the controller, we keep the Spring annotation org.springframework.web.bind.annotation.RequestBody, because only the Spring one binds the JSON body to the parameter. If we need both in one class, we write the Swagger one with its full package name.

3.5. Setting the API Title and Version

The title and the version at the top of Swagger UI come from the info part of the OpenAPI document. We set them with a bean of type OpenAPI (from io.swagger.v3.oas.models), which springdoc-openapi uses as the starting point of the document.

@Configuration
public class OpenApiConfig {

  @Bean
  public OpenAPI recipesOpenApi() {
    return new OpenAPI()
        .info(new Info()
            .title("Recipes API")
            .version("1.0")
            .description("Create and read recipes"));
  }
}

With all the annotations in place, Swagger UI shows the API title and one Recipes group with a summary for each operation.

Swagger UI page of the Recipes API, version 1.0, OAS 3.1, with the URL /v3/api-docs in the top bar. The Recipes group, described as Create and read recipes, lists GET /api/recipes List all recipes, POST /api/recipes Create a recipe and GET /api/recipes/{id} Find a recipe by its id. A Schemas section lists the Recipe object.
Swagger UI shows the title from the OpenAPI bean, the group from @Tag and the summaries from @Operation.

4. Trying an Endpoint in Swagger UI

Each operation in Swagger UI has a “Try it out” button. We click it, fill in the parameters and press Execute, and Swagger UI sends a real HTTP request to the running app. The result shows the matching curl command and the full response, including the status code and the headers.

Swagger UI with the operation GET /api/recipes/{id} opened in Try it out mode. The id parameter is 1. Below the Execute button, the Curl box shows curl -X GET http://localhost:8080/api/recipes/1 with the header accept: application/json, and the server response is code 200 with the body id 1, name Pancakes, minutes 20 and the header content-type application/json.
“Try it out” sends a real request to the app and shows the curl command and the response.

Because the requests are real, a POST in Swagger UI creates a real recipe, and a DELETE removes real data. So we use Swagger UI against a local or test environment, and in production we turn it off, as shown in section 5.2.

5. Configuring springdoc-openapi

The springdoc-openapi library reads its settings from application.properties with the prefix springdoc. Every property has a default, so we set only the ones we want to change.

PropertyDefaultWhat it changes
springdoc.api-docs.path/v3/api-docsURL of the OpenAPI document
springdoc.swagger-ui.path/swagger-ui.htmlURL of Swagger UI
springdoc.api-docs.enabledtrueSet to false to remove the OpenAPI endpoints
springdoc.swagger-ui.enabledtrueSet to false to remove Swagger UI
springdoc.packagesToScanall packagesDocuments only the controllers in the listed packages
springdoc.pathsToMatchall pathsDocuments only the paths that match the listed patterns, such as /api/\\**

5.1. Changing the Swagger UI and API Docs URLs

Some teams publish all API documentation under one URL, such as /docs. We set both paths in application.properties.

springdoc.swagger-ui.path=/docs
springdoc.api-docs.path=/api-docs

With these settings, /docs redirects to /swagger-ui/index.html, and the OpenAPI document moves to /api-docs. The old URLs /swagger-ui.html and /v3/api-docs return 404. The Swagger UI files themselves stay under /swagger-ui/.

5.2. Disabling Swagger UI in Production

The docs show every endpoint and every field of our API, and “Try it out” can change real data. For a public production API, we turn off both endpoints. We put the two properties into a profile-specific file, so the docs stay on for local development.

# No API docs and no Swagger UI in production
springdoc.api-docs.enabled=false
springdoc.swagger-ui.enabled=false

When we start the app with –spring.profiles.active=prod, the docs URLs return 404 and the API keeps working. The two startup warnings disappear as well. The Spring Boot profiles guide explains the other ways to activate a profile.

RequestDefault profileprod profile
GET /v3/api-docs200404
GET /swagger-ui.html302404
GET /api/recipes200200

6. Migrating From Springfox to springdoc-openapi

Many older Spring Boot projects still use Springfox with @EnableSwagger2 and a Docket bean. Springfox has had no release since 3.0.0 in July 2020 and it was built for the javax.servlet API, whereas Spring Boot 3 and 4 use the jakarta.servlet API. So a Spring Boot 3 or 4 project moves to springdoc-openapi.

The springdoc-openapi migration guide describes four steps.

  1. Remove the Springfox dependencies and add springdoc-openapi-starter-webmvc-ui.
  2. Replace the Swagger 2 annotations with the Swagger 3 annotations from the table.
  3. Remove the Docket bean. If it selected packages or paths, set springdoc.packagesToScan or springdoc.pathsToMatch instead. Several Docket beans become GroupedOpenApi beans.
  4. Move the API title and version from the ApiInfo object into an OpenAPI bean, as in section 3.5.

Most annotations have a direct replacement, but the attribute names change, for example code becomes responseCode in @ApiResponse.

Springfox (Swagger 2)springdoc-openapi (Swagger 3)
@Api@Tag
@ApiOperation(value = “foo”, notes = “bar”)@Operation(summary = “foo”, description = “bar”)
@ApiResponse(code = 404, message = “foo”)@ApiResponse(responseCode = “404”, description = “foo”)
@ApiParam@Parameter
@ApiImplicitParam@Parameter
@ApiImplicitParams@Parameters
@ApiModel@Schema
@ApiModelProperty@Schema
@ApiModelProperty(allowEmptyValue = true)@Schema(nullable = true)
@ApiIgnore@Hidden, @Operation(hidden = true) or @Parameter(hidden = true)

The URLs change too. Springfox served the document at /v2/api-docs, and springdoc-openapi serves it at /v3/api-docs, so API gateways and client generators that read the old URL need the new one.

7. Swagger Documentation FAQs

7.1. Why Does the App Fail to Start After Upgrading to Spring Boot 4?

The springdoc-openapi version is too old. Version 2.x was compiled against Spring Boot 3, and Spring Boot 4 moved the class WebMvcProperties to a new package. With springdoc-openapi 2.8.17 on Spring Boot 4.1.1, the app stops at startup with this error.

ERROR o.s.boot.SpringApplication : Application run failed
Caused by: org.springframework.boot.autoconfigure.condition.OnBeanCondition$BeanTypeDeductionException: Failed to deduce bean type for org.springdoc.webmvc.ui.SwaggerConfig.swaggerWelcome
Caused by: java.lang.NoClassDefFoundError: org/springframework/boot/autoconfigure/web/servlet/WebMvcProperties

We fix it by upgrading to springdoc-openapi 3.x, as the version table in section 2.1 shows.

7.2. Why Does /swagger-ui.html Return 404?

Swagger UI is not served at the URL we call. We check these causes in order.

  • The property springdoc.swagger-ui.enabled is false, for example in an active prod profile.
  • The property springdoc.swagger-ui.path moves Swagger UI to another URL, so /swagger-ui.html no longer exists.
  • The app sets a context path with server.servlet.context-path, such as /shop, so Swagger UI is at /shop/swagger-ui.html.
  • The project has the springdoc-openapi-starter-webmvc-api starter, which has no Swagger UI.

7.3. How Do I Hide an Endpoint From Swagger Documentation?

We put @Hidden on the method, or on the controller class to hide all its endpoints. The @Operation(hidden = true) attribute does the same for one method. In our example, deleteAll() clears the test data, and it does not show up in the OpenAPI document.

@Hidden
@DeleteMapping
public ResponseEntity<Void> deleteAll() {
  recipes.clear();
  return ResponseEntity.noContent().build();
}

The @Hidden annotation removes the endpoint from the docs, not from the app. A DELETE /api/recipes request still returns 204 and clears the recipes, so an endpoint that must not be public needs security, not @Hidden.

7.4. Does Swagger UI Work With Spring Security?

Yes, but Spring Security protects the docs URLs like every other URL, so Swagger UI returns 401 until we permit them. We add the docs paths to permitAll() and keep the API itself protected.

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
  http
      .authorizeHttpRequests(auth -> auth
          .requestMatchers("/v3/api-docs/**", "/v3/api-docs.yaml",
              "/swagger-ui.html", "/swagger-ui/**").permitAll()
          .anyRequest().authenticated())
      .httpBasic(Customizer.withDefaults());
  return http.build();
}

The pattern /v3/api-docs/\\** covers /v3/api-docs and /v3/api-docs/swagger-config, which Swagger UI loads first. It does not cover /v3/api-docs.yaml, so the YAML URL needs its own entry. With this configuration, the docs URLs return 200 or 302, and GET /api/recipes without credentials returns 401.

7.5. How Do I Download the OpenAPI Document as a File?

We download it from the running app. The JSON document is at /v3/api-docs, and the YAML document is at /v3/api-docs.yaml.

curl -o recipes-api.json http://localhost:8080/v3/api-docs
curl -o recipes-api.yaml http://localhost:8080/v3/api-docs.yaml

We can import the file into Postman or give it to a client code generator.

8. Conclusion

Swagger documentation is an OpenAPI document plus the Swagger UI page that renders it. In a Spring Boot application, the springdoc-openapi starter generates both from our controllers, and its major version must match the Spring Boot major version, which means 3.x for Spring Boot 4.

The generated document is correct for paths and types, but it cannot know status codes such as 201 and 404 or what a field means. We add that with @Tag, @Operation, @ApiResponse, @Parameter and @Schema, and the Bean Validation constraints show up in the schema without extra annotations.

Before going live, we decide where the docs may run. A prod profile with both enabled properties set to false removes them, and with Spring Security, we permit only the docs paths.

9. References

Happy Learning !!

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.