API Documentation with Swagger and OpenAPI

This post links to: Building a Spring Boot CRUD App With Postgres from Scratch: The Complete Guide.

This guide follows on from Testing APIs in Spring Boot.

In this post, we’ll add some Swagger documentation to our Spring Boot app using Springdoc OpenAPI to help inform users how to interact with our API. We’ll walk through the setup of Swagger, how to annotate our endpoints and models, and how to customise the Swagger UI.

What is Swagger

Swagger is an open-source framework for designing, building, documenting, and consuming RESTful APIs. It provides a standard way to describe your API using the OpenAPI Specification.

The OpenAPI Specification is a language-agnostic standard for describing REST APIs. It’s essentially a structured .json or .yaml file that describes the following:

The Swagger tools work with the OpenAPI spec to generate documentation, client SDKs, test UIs, and more.

How Swagger Works

  1. Firstly, you'll need an application that defines REST endpoints. In Java/Spring this would include using annotations like @RestController, @GetMapping, @PostMapping etc. Luckily, we've already built one!
  2. You can then use Swagger annotations (e.g. @Operation, @Schema, @ApiResponse) to enrich your API with extra metadata.
  3. Springdoc (the Swagger core library) will scan your annotated code at runtime, automatically generate the OpenAPI JSON spec, and then exposes it at: /v3/api-docs
  4. The Swagger UI is a web-based UI that can read the automatically generated OpenAPI spec at /v3/api-docs and can render interactive documentation at: /swagger-ui.html
  5. This allows users to interact with the live API from the browser, send test requests, inspect responses, and understand all available operations.

Why Swagger

So why use Swagger? Well there are few reasons why.

TL;DR: Swagger turns your API into a self-explaining, interactive, developer-friendly tool.

Let's get into setting up Swagger for our API!

Setting Up Dependencies

First things first, we're going to need to add the following dependency to our build.grade file.

// build.gradle
dependencies {
    //other deps...
    implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.5'
    // other deps...
}

Viewing the Swagger UI

Now we've added our dependency, that should be it in terms of the minimal amount of setup needed for Swagger.

We can run our application and view our automatically generated Swagger UI page by visiting the following URL: http://localhost:8080/swagger-ui.html

You should see something similar to this:

swagger-ui-html-page.png

We can also view the raw OpenAPI JSON used to populate the Swagger UI by visiting: http://localhost:8080/v3/api-docs

It's as easy as that!

Swagger Annotations

We can use various Swagger annotations to customise the Swagger UI and provide our API users with more detailed information.

Swagger Controller Annotations

We can start with some basic annotations for our controller:

Here's a full example of applying the Swagger annotations to the UserController

// UserController.java
@RestController
@RequestMapping("/api/users")
@Tag(name = "Users", description = "CRUD operations for managing users")
public class UserController {
    
    //...dependencies

    @Operation(summary = "Get all users", description = "Returns a list of all users stored in the database")
    @ApiResponse(responseCode = "200", description = "List of users returned successfully")
    @GetMapping
    public List<UserResponse> getAllUsers() {
        //...logic
    }

    @Operation(summary = "Get a user by their ID", description = "Returns a user with the specified ID")
    @ApiResponses({
            @ApiResponse(responseCode = "200", description = "User found and returned"),
            @ApiResponse(responseCode = "404", description = "User not found")
    })
    @GetMapping("/{id}")
    public UserResponse getUserById(@PathVariable("id") final Long id) {
        //...logic
    }

    @Operation(summary = "Create a new user", description = "Creates and returns the newly created user")
    @ApiResponses({
            @ApiResponse(responseCode = "200", description = "User created successfully"),
            @ApiResponse(responseCode = "400", description = "Invalid request data")
    })
    @PostMapping
    public UserResponse createUser(@Valid @RequestBody final UserCreateRequest user) {
        //...logic
    }

    @Operation(summary = "Update a user", description = "Updates a user by ID with the provided data")
    @ApiResponses({
            @ApiResponse(responseCode = "200", description = "User updated successfully"),
            @ApiResponse(responseCode = "404", description = "User not found"),
            @ApiResponse(responseCode = "400", description = "Invalid request data")
    })
    @PutMapping("/{id}")
    public UserResponse updateUser(@PathVariable("id") final Long id,
                                   @Valid @RequestBody final UserUpdateRequest updatedUser) {
        //...logic
    }

    @Operation(summary = "Delete a user", description = "Deletes a user by ID")
    @ApiResponses({
            @ApiResponse(responseCode = "200", description = "User deleted successfully"),
            @ApiResponse(responseCode = "404", description = "User not found")
    })
    @DeleteMapping("/{id}")
    public void deleteUser(@PathVariable("id") final Long id) {
        //...logic
    }
}

You can now visit http://localhost:8080/swagger-ui.html to see the extra information that's been added to the UI:

swagger-ui-controller-top-level-example.png

Swagger Model Annotations

We can also use the following annotations on the data models for our API to give the user a better idea about any necessary requirements. For example whether a field for a model is required or if a String field on a model must be under a certain length.

Here's a full example of applying the Swagger annotations to UserCreateRequest, UserResponse and UserUpdateRequest:

UserCreateRequest

// UserCreateRequest.java
@Schema(description = "Payload to create a new user")
public class UserCreateRequest {

    @Schema(description = "Full name of the user", example = "Jon Smith", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "Name is required")
    @Size(min = 2, max = 100, message = "Name must be between 2 and 100 characters")
    private String name;

    @Schema(description = "Email address of the user", example = "jon@example.com", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "Email is required")
    @Email(message = "Email must be valid")
    private String email;

    @Schema(description = "Age of the user", example = "30", requiredMode = Schema.RequiredMode.REQUIRED)
    @Min(value = 18, message = "Age should not be less than 18")
    @Max(value = 150, message = "Age should not be greater than 150")
    private int age;
    // Rest of class
}

UserResponse

// UserResponse.java
@Schema(description = "Response object for a user")
public class UserResponse {

    @Schema(description = "Unique ID of the user", example = "1")
    private Long id;

    @Schema(description = "Full name of the user", example = "Jon Smith")
    private String name;

    @Schema(description = "Email address of the user", example = "jon@example.com")
    private String email;

    @Schema(description = "Age of the user", example = "30")
    private int age;
    // Rest of class
}

UserUpdateRequest

// UserUpdateRequest.java
@Schema(description = "Payload to update an existing user")
public class UserUpdateRequest {

    @Schema(description = "Updated name of the user", example = "Jon Smith Updated", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "Name is required")
    @Size(min = 2, max = 100, message = "Name must be between 2 and 100 characters")
    private String name;

    @Schema(description = "Updated email address of the user", example = "jon-updated@example.com", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "Email is required")
    @Email(message = "Email must be valid")
    private String email;

    @Schema(description = "Updated age of the user", example = "35", requiredMode = Schema.RequiredMode.REQUIRED)
    @Min(value = 18, message = "Age should not be less than 18")
    @Max(value = 150, message = "Age should not be greater than 150")
    private int age;
    // Rest of class
}

You can now visit http://localhost:8080/swagger-ui.html to see the extra information that's been added to the UI:

swagger-ui-model-example.png

Swagger Configuration

We can also apply different configuration options to Swagger using the application.yml properties file. Here's an example configuration:

# application.yml
springdoc:
  swagger-ui:
    enabled: ${SWAGGER_ENABLED:true}
    path: /docs
    operationsSorter: method
    tagsSorter: alpha
  api-docs:
    path: /doc-json

That's great, but what do these configuration options mean. Well:

You can find all other configuration properties for Swagger here.

What's Next?

So to recap we:

GitHub Example

Stay tuned for the upcoming blog in the series about Securing Your API with Spring Security!