Using Swagger 3 in Spring Boot 3

Fady Kuzman
6 min readJan 29, 2023

Apparently, Spring Boot 3 needs a different library than Spring Boot 2 to be able to use Swagger 3. Let’s set it up in a new project and see how to use the most basic features.

NOTE: You can find the code on github. For the lists of imports and the full pom.xml please go there.

Step 1: Use your favorite tool to generate a new spring application. For example https://start.spring.io/

Step 2: Add the following maven dependency, as mentioned in the docs

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

After letting your IDE download and reload the dependencies, you can run

$ mvn dependency:tree

You will find a block in the output similar to the one below, showing all the transitive dependencies that org.springdoc:springdoc-openapi-starter-webmvc-ui pulled in. Double check that io.swagger.core.v3:swagger-core-jakarta is there.

A snippet of the mvn dependency:tree command results

Step 3: Run the application using the following command, or by running it from the IDE.

$ mvn spring-boot:run

Go to your browser and navigate to localhost:8080/swagger-ui.html

localhost and 8080 are the default for server name and port respectively. If you are running the application with different values, change them accordingly. Also, if you have a context path, which is usually set in your properties file you should add it.

Setting a context path in application.properties or application.yml:

server:
servlet:
context-path: /api/something

Generic path to Swagger UI

http://server:port/context-path/swagger-ui.html

By this time, we had not written any code. No controllers, no Api definitions. Therefore, what comes next shouldn’t be surprising.

First view of the your API documentation without any endpoints defined in the code

That’s what you’ll see. No operations. Some general information. Just be sure there are no errors.

Step 4a: Add a Controller and Entity

Just a simple one. Don’t be fancy. I am returning a null here because I don’t really care if the controller returns something meaningful. What I care about is the return type in the method signature and the GetMapping annotation.

@RestController
@RequestMapping("/plant/all")
public class PlantController {

@GetMapping
public ResponseEntity<List<Plant>> getAll() {
return ResponseEntity.ok(null);
}
}

I like to use lombok, that explains the Data annotation and the lack of setters and getters. Which saves you a lot of boilerplate. But don’t get hung on that. Just put in your setters and getters, if you prefer.

@Data
public class Plant {
private Long id;
private String name;
private String description;
}

Step 4b: Add a Service to return some dummy data and at the same time keep the controller clean to focus on Swagger.

@Service
public class PlantService {

private List<Plant> plants = new ArrayList<>(){
{
add(new Plant(1L, "plant1", "this is plant1"));
add(new Plant(2L, "plant2", "this is plant2"));
add(new Plant(3L, "plant3", "this is plant3"));
}

};

public List<Plant> getAll() {
return plants;
}
}

In case you are confused by my code structure in the github repository and asking yourself, why on earth is he not separating his controllers, entities and services? Please refer to my post on that topic. For now keep reading on and go back later. I will refer to it again at the end.

Step 4c: Restart the application

Refresh your page or navigate to the URL: localhost:8080/swagger-ui.html. Now you will see the endpoint that you just wrote.

View of the GET method you added in the code

One more cool thing, is that it shows you the entity that is used by the endpoint.

Schema of your entity

It is important to say that swagger does that automatically, without you adding any annotation or any other code. We will see later that to go above and beyond the default workings of Swagger, you will have to add some annotations and it can get quite verbose.

Here ends the setting up of Swagger.

Next I will show you a few annotations to make your documentation more useful and more comprehensive.

Did you notice that ugly heading plant-controller on the top. Let’s make it go away and have something more user friendly and aesthetically and semantically pleasing. For that we need to use the class-level annotation Tag as shown below.

@Tag(name = "Plant", description = "the Plant Api")
@RestController
@RequestMapping("/plant/all")
public class PlantController {
// rest of the code
}

It takes, among others, two variables name and description. And the result is as you see in the image below.

View of the Header after applying the Tag annotation

How to give a description to the REST operation?

Use the Operation and ApiResponses annotation.

You can also see above how the GET operation doesn’t give you any explanation of what it does? You kind of have to figure it out on your own.

Let’s give it some more info. We will use the Operation annotation to give some info to the GET operation. And ApiResponses to give more info to the responses. Like what response codes and media types it produces.

@Tag(name = "Plant", description = "the Plant Api")
@RestController
@RequestMapping("/plant/all")
public class PlantController implements PlantApi{
@Autowired
private PlantService service;

@Operation(
summary = "Fetch all plants",
description = "fetches all plant entities and their data from data source")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "successful operation")
})
@GetMapping(value = "/all", produces = "application/json")
public ResponseEntity<List<Plant>> getAll() {
var plants = service.getAll();
return ResponseEntity.ok(plants);
}
}

Now let’s check out the result. Rerun the application and go to and refresh the documentation web page.

View after adding the Operation annotation
View after adding the ApiResponses annotation

Here you see all the things properly populated as you wrote them in the annotations.

Oh this is getting crowded! How about we move all that somewhere else, so we can actually see the implementation?

Let’s create an interface, call it PlantApi or something of the sort.

Add an abstract method, in this case called getAll() as shown below.

Move the Tag, Operation and ApiResponeses annotations to the PlantApi as shown below.

@Tag(name = "Plant", description = "the Plant Api")
public interface PlantApi {

@Operation(
summary = "Fetch all plants",
description = "fetches all plant entities and their data from data source")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "successful operation")
})
ResponseEntity<List<Plant>> getAll();
}

Then remove Tag, Operation and ApiResponses annotations from the PlantController and let it implement the PlantApi.

@RestController
@RequestMapping("/plant/all")
public class PlantController implements PlantApi {
@Autowired
private PlantService service;

@Override
@GetMapping(value = "/all", produces = "application/json")
public ResponseEntity<List<Plant>> getAll() {
var plants = service.getAll();
return ResponseEntity.ok(plants);
}
...
}

Let’s rerun the application and go to the Swagger web page.

View after extracting the tags to an interface to keep our implementation clean

Look at that! nothing changed. We successfully refactored the code. :D

If you want to see how I implemented a POST method. Go check it on github.

I hope that was helpful. I know it is not that hard, but some things can be daunting in the beginning. Just try to have fun and enjoy the learning journey.

If you like my style and want me to write a post about a specific topic, go ahead and write it in the comments.

--

--