{{announcement.body}}
{{announcement.title}}

Doing More With Springdoc-OpenAPI

DZone 's Guide to

Doing More With Springdoc-OpenAPI

Rendering fully qualified names in the generated swagger documentation and global exception handling using controller advice.

· Java Zone ·
Free Resource

In my last recent article we tried out a Spring Boot Open API 3-enabled REST project and explored some of its capabilities namely:

  • Automatic JSR-303 related swagger documentation.
  • How maven builds properties could be shown as project information in the swagger documentation.

In this continuation we will explore two additional Objectives namely:

  • Rendering Fully Qualified names in the generated swagger documentation.
  • Global Exception Handling Using Controller Advice and its related swagger documentation.

We are going to refer to https://spring.io/guides/gs/rest-service/ and https://springdoc.org/ like last time.

Prerequisites

  • Java 8.x.
  • Maven 3.x.

Steps

The code is loosely based on the last article but there are some differences/additions to the last article. We will start by listing the new code. We will then run the application. As we walk through this tutorial listing the various artifacts we will discuss which code helped in achieving the objectives of this tutorial along with some other details.

The code is a typical java maven project and is laid out as shown below

java maven

Listing below the different artifacts one by one. Try following the above layout:

Showing pom.xml

XML
 




xxxxxxxxxx
1
53


 
1
<?xml version="1.0" encoding="UTF-8"?>
2
<project xmlns="http://maven.apache.org/POM/4.0.0"
3
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
5
    <modelVersion>4.0.0</modelVersion>
6
    <parent>
7
        <groupId>org.springframework.boot</groupId>
8
        <artifactId>spring-boot-starter-parent</artifactId>
9
        <version>2.2.2.RELEASE</version>
10
        <relativePath ></relativePath> <!-- lookup parent from repository -->
11
    </parent>
12
    <groupId>com.example</groupId>
13
    <artifactId>sample</artifactId>
14
    <version>0.0.1</version>
15
    <name>sample</name>
16
    <description>Demo project for Spring Boot with openapi 3 documentation</description>
17
18
    <properties>
19
        <java.version>1.8</java.version>
20
    </properties>
21
22
    <dependencies>
23
        <dependency>
24
            <groupId>org.springframework.boot</groupId>
25
            <artifactId>spring-boot-starter-web</artifactId>
26
        </dependency>
27
        
28
        <dependency>
29
            <groupId>org.springdoc</groupId>
30
            <artifactId>springdoc-openapi-ui</artifactId>
31
            <version>1.3.2</version>
32
        </dependency>
33
        <dependency>
34
            <groupId>org.springframework.boot</groupId>
35
            <artifactId>spring-boot-starter-test</artifactId>
36
            <scope>test</scope>
37
            <exclusions>
38
                <exclusion>
39
                    <groupId>org.junit.vintage</groupId>
40
                    <artifactId>junit-vintage-engine</artifactId>
41
                </exclusion>
42
            </exclusions>
43
        </dependency>
44
    </dependencies>
45
    <build>
46
        <plugins>
47
            <plugin>
48
                <groupId>org.springframework.boot</groupId>
49
                <artifactId>spring-boot-maven-plugin</artifactId>
50
            </plugin>
51
        </plugins>
52
    </build> 
53
</project>


SampleApplication.java follows:

Java
 






SampleApplication.java

The main improvement is the @PostConstruct init() method which will cause rendering of fully qualified names for java model classes in swagger schemas. Also as noted in the future possibly after a few releases the CustomConverter call can be avoided and FQNs can be enabled by just directly invoking io.swagger.v3.core.jackson.TypeNameResolver.std.setUseFqn(true); For now however we must deal with the CustomConverter.

CustomConverter.java follows:

Java
 




xxxxxxxxxx
1
37


 
1
package sample;
2
 
          
3
import java.util.Set;
4
 
          
5
import org.apache.commons.lang3.StringUtils;
6
 
          
7
import com.fasterxml.jackson.databind.ObjectMapper;
8
 
          
9
import io.swagger.v3.core.converter.ModelConverters;
10
import io.swagger.v3.core.jackson.ModelResolver;
11
import io.swagger.v3.core.jackson.TypeNameResolver;
12
 
          
13
 
          
14
class CustomConverter extends ModelResolver {
15
 
          
16
    public static void add(ObjectMapper ojectMapper)
17
    {
18
        ModelConverters.getInstance().addConverter(new CustomConverter(ojectMapper));
19
    }
20
    public CustomConverter(ObjectMapper mapper) {
21
        super(mapper, new QualifiedTypeNameResolver());
22
    }
23
    
24
    static class QualifiedTypeNameResolver extends TypeNameResolver {
25
 
          
26
        @Override
27
        protected String nameForClass(Class<?> cls, Set<Options> options) {
28
            String className = cls.getName().startsWith("java.") ? cls.getSimpleName() : cls.getName();
29
            if (options.contains(Options.SKIP_API_MODEL)) {
30
                return className;
31
            }
32
            final io.swagger.v3.oas.annotations.media.Schema model = cls.getAnnotation(io.swagger.v3.oas.annotations.media.Schema.class);
33
            final String modelName = model == null ? null : StringUtils.trimToNull(model.name());
34
            return modelName == null ? className : modelName;
35
        }
36
    }
37
}


The key logic is in this line:

Java
 




xxxxxxxxxx
1


 
1
ModelConverters.getInstance().addConverter(new CustomConverter(ojectMapper));


The above-discussed code is enough to achieve fully qualified names of java model classes in swagger schemas. In the future, this can be simplified as discussed earlier.

Lets now move on to the main model class — Person.java

Java
 




xxxxxxxxxx
1
88


 
1
package sample;
2
 
          
3
import javax.validation.constraints.Email;
4
import javax.validation.constraints.Max;
5
import javax.validation.constraints.Min;
6
import javax.validation.constraints.NotBlank;
7
import javax.validation.constraints.NotNull;
8
import javax.validation.constraints.Pattern;
9
import javax.validation.constraints.Size;
10
 
          
11
 
          
12
import org.hibernate.validator.constraints.CreditCardNumber;
13
 
          
14
 
          
15
public class Person {
16
    private long id;
17
    private String firstName;
18
    @NotNull
19
    @NotBlank
20
    @Size(max = 10)
21
    private String lastName;
22
    @Pattern(regexp = ".+@.+\\..+", message = "Please provide a valid email address")
23
    private String email;
24
    @Email()
25
    private String email1;
26
    @Min(18)
27
    @Max(30)
28
    private int age;
29
    @CreditCardNumber
30
    private String creditCardNumber;
31
 
          
32
    public String getCreditCardNumber() {
33
        return creditCardNumber;
34
    }
35
 
          
36
    public void setCreditCardNumber(String creditCardNumber) {
37
        this.creditCardNumber = creditCardNumber;
38
    }
39
 
          
40
    public long getId() {
41
        return id;
42
    }
43
 
          
44
    public void setId(long id) {
45
        this.id = id;
46
    }
47
 
          
48
    public String getEmail1() {
49
        return email1;
50
    }
51
 
          
52
    public void setEmail1(String email1) {
53
        this.email1 = email1;
54
    }
55
 
          
56
    @Size(min = 2)
57
    public String getFirstName() {
58
        return firstName;
59
    }
60
 
          
61
    public void setFirstName(String firstName) {
62
        this.firstName = firstName;
63
    }
64
 
          
65
    public String getLastName() {
66
        return lastName;
67
    }
68
 
          
69
    public void setLastName(String lastName) {
70
        this.lastName = lastName;
71
    }
72
 
          
73
    public String getEmail() {
74
        return email;
75
    }
76
 
          
77
    public void setEmail(String email) {
78
        this.email = email;
79
    }
80
 
          
81
    public int getAge() {
82
        return age;
83
    }
84
 
          
85
    public void setAge(int age) {
86
        this.age = age;
87
    }
88
}


PersonController.java follows:

Java
 




xxxxxxxxxx
1
50


 
1
package sample;
2
 
          
3
import java.util.ArrayList;
4
import java.util.List;
5
import java.util.Random;
6
 
          
7
import javax.validation.Valid;
8
import javax.validation.constraints.NotBlank;
9
import javax.validation.constraints.NotNull;
10
import javax.validation.constraints.Size;
11
 
          
12
import org.springframework.validation.annotation.Validated;
13
import org.springframework.web.bind.annotation.RequestBody;
14
import org.springframework.web.bind.annotation.RequestMapping;
15
import org.springframework.web.bind.annotation.RequestMethod;
16
import org.springframework.web.bind.annotation.RequestParam;
17
import org.springframework.web.bind.annotation.RestController;
18
 
          
19
@RestController
20
@Validated
21
public class PersonController {
22
    private Random ran = new Random(); 
23
    @RequestMapping(path = "/person", method = RequestMethod.POST)
24
    public Person person(@Valid @RequestBody Person person) {
25
         
26
         int nxt = ran.nextInt(10); 
27
         if(nxt>=5)
28
         {
29
             throw new RuntimeException("Breaking logic");
30
         }
31
        return person;
32
    }
33
    @RequestMapping(path = "/personByLastName", method = RequestMethod.GET)
34
    public List<Person> findByLastName(@RequestParam(name = "lastName", required = true)@NotNull
35
            @NotBlank
36
            @Size(max = 10)String lastName){
37
        List<Person> hardCoded= new ArrayList<>();
38
        Person person= new Person();
39
        person.setAge(20);
40
        person.setCreditCardNumber("4111111111111111");
41
        person.setEmail("abc@abc.com");
42
        person.setEmail1("abc1@abc.com");
43
        person.setFirstName("Somefirstname");
44
        person.setLastName(lastName);
45
        person.setId(1);
46
        hardCoded.add(person);
47
        return hardCoded;
48
        
49
    }
50
}


The main changes are:

  • The addition of a GET method.
  • Use of some additional annotations like @Validated, @RequestParam, @NotNull, @NotBlank, @Size

Note: Use of @Validated causes validations to be applied even for the GET method parameter. After the application is complete and running play around with the above annotations. Feel free to remove them and see their effect. The GET URL can always be directly invoked using http://localhost:8080/personByLastName and http://localhost:8080/personByLastName?lastName=12345678901. For now, it's a little early. Let's continue adding remaining artifacts.

Let's discuss Global Exception handling using @ControllerAdvice and how we can achieve corresponding swagger documentation.

When we come to ExceptionHandling in Spring Controllers there are so many Exceptions possible. There may be various validation related exceptions invoked even before the actual controller code is invoked. 

The controller itself my have some logic that leads to a business Exception or some Exception because of bad coding. Rather than handling these exceptions in each Controller, we are choosing to duck the Exceptions. We are choosing to centralize the exception handling be it during the controller invocation or before.

Producing below a very rudimentary ControllerAdvice — GlobalControllerAdvice.java which you can later expand upon and improve as needed.

Java
 




xxxxxxxxxx
1
131


 
1
package sample;
2
 
          
3
import java.util.ArrayList;
4
import java.util.List;
5
import java.util.Set;
6
import java.util.UUID;
7
 
          
8
import javax.validation.ConstraintViolation;
9
import javax.validation.ConstraintViolationException;
10
 
          
11
import org.slf4j.Logger;
12
import org.slf4j.LoggerFactory;
13
import org.springframework.http.HttpStatus;
14
import org.springframework.http.MediaType;
15
import org.springframework.http.ResponseEntity;
16
import org.springframework.http.converter.HttpMessageNotReadableException;
17
import org.springframework.validation.FieldError;
18
import org.springframework.validation.ObjectError;
19
import org.springframework.web.HttpMediaTypeNotSupportedException;
20
import org.springframework.web.bind.MethodArgumentNotValidException;
21
import org.springframework.web.bind.MissingServletRequestParameterException;
22
import org.springframework.web.bind.annotation.ControllerAdvice;
23
import org.springframework.web.bind.annotation.ExceptionHandler;
24
import org.springframework.web.bind.annotation.RequestMapping;
25
import org.springframework.web.bind.annotation.ResponseStatus;
26
 
          
27
 
          
28
 
          
29
@ControllerAdvice @RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE) 
30
public class GlobalControllerAdvice //extends ResponseEntityExceptionHandler  
31
{
32
    /**
33
     * Note use base class if you wish to leverage its handling.
34
     * Some code will need changing.
35
     */
36
    private static final Logger logger = LoggerFactory.getLogger(GlobalControllerAdvice.class);
37
    
38
    @ExceptionHandler(Throwable.class) 
39
    @ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR)
40
    public ResponseEntity < Problem > problem(final Throwable e) {
41
        String message =e.getMessage();
42
        //might actually prefer to use a geeric mesasge
43
        
44
        message="Problem occured";
45
        UUID uuid = UUID.randomUUID();
46
        String logRef=uuid.toString();
47
        logger.error("logRef="+logRef, message, e);
48
        return new ResponseEntity <Problem> (new Problem(logRef, message), HttpStatus.INTERNAL_SERVER_ERROR);
49
    }
50
   
51
    
52
    
53
    @ExceptionHandler(MethodArgumentNotValidException.class) 
54
    @ResponseStatus(code = HttpStatus.BAD_REQUEST)
55
    public ResponseEntity<ErrorMessage> handleMethodArgumentNotValid(MethodArgumentNotValidException ex
56
            ) {
57
        List<FieldError> fieldErrors = ex.getBindingResult().getFieldErrors();
58
        List<ObjectError> globalErrors = ex.getBindingResult().getGlobalErrors();
59
        List<String> errors = new ArrayList<>(fieldErrors.size() + globalErrors.size());
60
        String error;
61
        for (FieldError fieldError : fieldErrors) {
62
            error = fieldError.getField() + ", " + fieldError.getDefaultMessage();
63
            errors.add(error);
64
        }
65
        for (ObjectError objectError : globalErrors) {
66
            error = objectError.getObjectName() + ", " + objectError.getDefaultMessage();
67
            errors.add(error);
68
        }
69
        ErrorMessage errorMessage = new ErrorMessage(errors);
70
      
71
        //Object result=ex.getBindingResult();//instead of above can allso pass the more detailed bindingResult
72
        return new ResponseEntity(errorMessage, HttpStatus.BAD_REQUEST);
73
    }
74
    @ExceptionHandler(ConstraintViolationException.class) 
75
    @ResponseStatus(code = HttpStatus.BAD_REQUEST)
76
    public ResponseEntity<ErrorMessage> handleConstraintViolatedException(ConstraintViolationException ex
77
            ) {
78
        Set<ConstraintViolation<?>> constraintViolations = ex.getConstraintViolations();
79
        
80
       
81
        List<String> errors = new ArrayList<>(constraintViolations.size() );
82
        String error;
83
        for (ConstraintViolation constraintViolation : constraintViolations) {
84
            
85
            error =  constraintViolation.getMessage();
86
            errors.add(error);
87
        }
88
     
89
        ErrorMessage errorMessage = new ErrorMessage(errors);
90
        return new ResponseEntity(errorMessage, HttpStatus.BAD_REQUEST);
91
    }
92
    
93
    @ExceptionHandler(MissingServletRequestParameterException.class) 
94
    @ResponseStatus(code = HttpStatus.BAD_REQUEST)
95
    public ResponseEntity<ErrorMessage> handleMissingServletRequestParameterException(MissingServletRequestParameterException ex
96
            ) {
97
        
98
       List<String> errors = new ArrayList<>( );
99
        String error=ex.getParameterName()+", "+ex.getMessage();
100
       errors.add(error);
101
        ErrorMessage errorMessage = new ErrorMessage(errors);
102
         return new ResponseEntity(errorMessage, HttpStatus.BAD_REQUEST);
103
    }
104
 
          
105
   
106
    @ExceptionHandler(HttpMediaTypeNotSupportedException.class) 
107
    @ResponseStatus(code = HttpStatus.UNSUPPORTED_MEDIA_TYPE)
108
    public ResponseEntity<ErrorMessage> handleHttpMediaTypeNotSupported(HttpMediaTypeNotSupportedException ex 
109
            ) {
110
        String unsupported = "Unsupported content type: " + ex.getContentType();
111
        String supported = "Supported content types: " + MediaType.toString(ex.getSupportedMediaTypes());
112
        ErrorMessage errorMessage = new ErrorMessage(unsupported, supported);
113
        return new ResponseEntity(errorMessage, HttpStatus.UNSUPPORTED_MEDIA_TYPE);
114
    }
115
 
          
116
    @ExceptionHandler(HttpMessageNotReadableException.class) 
117
    @ResponseStatus(code = HttpStatus.BAD_REQUEST)
118
    public ResponseEntity<ErrorMessage> handleHttpMessageNotReadable(HttpMessageNotReadableException ex) {
119
        Throwable mostSpecificCause = ex.getMostSpecificCause();
120
        ErrorMessage errorMessage;
121
        if (mostSpecificCause != null) {
122
            String exceptionName = mostSpecificCause.getClass().getName();
123
            String message = mostSpecificCause.getMessage();
124
            errorMessage = new ErrorMessage(exceptionName, message);
125
        } else {
126
            errorMessage = new ErrorMessage(ex.getMessage());
127
        }
128
        return new ResponseEntity(errorMessage,  HttpStatus.BAD_REQUEST);
129
    }
130
   
131
}


The key thing to remember is to decorate the methods in this class with @org.springframework.web.bind.annotation.ResponseStatus which is the only hint that spring doc Open-API needs for including the appropriate models in its controller method swagger documentation.

For completeness will list below two classes referred to in this GlobalControllerAdvice.java before we attempt to start the application.

Problem.java follows:

Java
 




xxxxxxxxxx
1
34


 
1
package sample;
2
 
          
3
public class Problem {
4
    
5
private String logRef;
6
private String message;
7
 
          
8
public Problem(String logRef, String message) {
9
    super();
10
    this.logRef = logRef;
11
    this.message = message;
12
}
13
 
          
14
public Problem() {
15
    super();
16
    
17
}
18
 
          
19
public String getLogRef() {
20
    return logRef;
21
}
22
 
          
23
public void setLogRef(String logRef) {
24
    this.logRef = logRef;
25
}
26
 
          
27
public String getMessage() {
28
    return message;
29
}
30
 
          
31
public void setMessage(String message) {
32
    this.message = message;
33
}
34
}


ErrorMessage.java also follows:

Java
 




xxxxxxxxxx
1
34


 
1
package sample;
2
 
          
3
import java.util.Arrays;
4
import java.util.Collections;
5
import java.util.List;
6
 
          
7
 
          
8
public class ErrorMessage {
9
 
          
10
    private List<String> errors;
11
 
          
12
    public ErrorMessage() {
13
    }
14
 
          
15
    public ErrorMessage(List<String> errors) {
16
        this.errors = errors;
17
    }
18
 
          
19
    public ErrorMessage(String error) {
20
        this(Collections.singletonList(error));
21
    }
22
 
          
23
    public ErrorMessage(String ... errors) {
24
        this(Arrays.asList(errors));
25
    }
26
 
          
27
    public List<String> getErrors() {
28
        return errors;
29
    }
30
 
          
31
    public void setErrors(List<String> errors) {
32
        this.errors = errors;
33
    }
34
}


Let's Not Forget Application.Properties:

Properties files
 




xxxxxxxxxx
1


 
1
application-description=@project.description@
2
application-version=@project.version@



That completes all our code for this tutorial. Let's try it out.

Execute the mvn clean package from the command prompt or terminal. Then, execute java -jar target\sample-0.0.1.jar

You can also launch the application by running the SampleApplication.java class from your IDE.

Now, let's visit the Swagger UI — http://localhost:8080/swagger-ui.html. We can also visit the JSON based API-docs - http://localhost:8080/v3/api-docs. For YAML based API-docs use  http://localhost:8080/v3/api-docs.yaml. These URLs are the defaults and can be changed if needed.

General usage of swagger UI is explained in the last article. In case of issues please see the Troubleshooting section at the bottom of this tutorial.

Will focus on this tutorial specific areas.

Now that the application should be running let's visit it.

The swagger UI landing screen — http://localhost:8080/swagger-ui.html.

sample application API

Sample application API

Note the fully qualified class names.

Now let's see how controller advice has contributed to the swagger documentation.

default response

Default response

If we were to click the schema links it will look like this.

default response code

Default response code

So by adding  @ResponseStatus to our ControllerAdvice methods, we can see the related schemas associated with the HTTPStatus for each of the REST controller methods documentation. How neat is that?

Note: @ControllerAdvice also has some attributes (e.g. "assignableTypes"), which allow the @ControllerAdvice to be applied more specifically on Controllers than globally. Currently, that behavior is not there yet, but please expect it to be there when the springdoc-openapi folks release their next version.

With the above, we have completed the two goals we had set out to achieve.

A little manual testing follows.

The Post Method

Java
 




xxxxxxxxxx
1
10


 
1
@RequestMapping(path = "/person", method = RequestMethod.POST)
2
    public Person person(@Valid @RequestBody Person person) {
3
         
4
         int nxt = ran.nextInt(10); 
5
         if(nxt>=5)
6
         {
7
             throw new RuntimeException("Breaking logic");
8
         }
9
        return person;
10
    }



This method is designed to have automatic validations. That was tested in the last article.

This method is also designed to cause exceptions randomly just to see how the controller's advice is invoked when things go wrong.

Lets test.

Click the green Post button. Then click the Try it out button. Then click the blue execute button.

Parameters

Parameters

Note that the validation errors are crisper compared to the last article.

JSON
 




xxxxxxxxxx
1


 
1
{
2
  "errors": [
3
    "age, must be greater than or equal to 18",
4
    "creditCardNumber, invalid credit card number",
5
    "email, Please provide a valid email address",
6
    "email1, must be a well-formed email address"
7
  ]
8
}


The validation errors are now crisper because of the logic in the controller advice.

Lets now feed it valid input. Showing here valid input for ready reference that can be copy-pasted.

JSON
 




xxxxxxxxxx
1


 
1
{
2
  "firstName": "string",
3
  "lastName": "string",
4
  "email": "string@email1.com",
5
  "email1": "strin@email2.comg",
6
  "age": 18,
7
  "creditCardNumber": "4111111111111111"
8
}



Copy the valid input into the below screen as shown below and press the blue execute button.

post parameters

Post parametes

This will cause either of the two below responses depending on whether the random exception occurs or not:

 server response

Server response


 server response

Server response


 Without the exception

 When the exception happened

Note again the response for the Http Status code of 500 is formatted as per logic in the ControllerAdvice.

The GET Method

Lets now examine the GET method in our Controller.

Java
 




x


 
1
@RequestMapping(path = "/personByLastName", method = RequestMethod.GET)
2
    public List<Person> findByLastName(@RequestParam(name = "lastName", required = true)@NotNull
3
            @NotBlank
4
            @Size(max = 10)String lastName){
5
        List<Person> hardCoded= new ArrayList<>();
6
        //skipping the details
7
        return hardCoded;
8
        
9
    }



Also, note that the controller class is decorated with a @Validated annotation. 

There can be a few minor limitations. I will explore one of them without spending too much time on it. As we saw in our last article as of now it does not fully cover all the JSR 303 annotations. There are other issues also some of which are not entirely due to springdoc open API but rather more likely due to the swagger UI project itself.

Let's click the blue GET button followed by the Try it out button.

/personbyLastName

/personbyLastName

Let's feed-in 12345678901 for the lastName input field and press the blue Execute button.

person-controller

Person controller

There can be some improvement in how the contract expected from the validations is displayed.

As discussed earlier the GET URL can always be directly invoked using http://localhost:8080/personByLastName and http://localhost:8080/personByLastName?lastName=12345678901. Give it a try to see what are the responses. Play with the annotations @Validated, @RequestParam, @NotNull, @NotBlank, @Size and see how the handling or contract changes. A spring developer who also is a user of the generated documentation will expect the documentation to be corresponding to his usage of these and other annotations. 

It's possible there are some minor areas where this may not be perfect but the bigger strong vital pieces are all there. It covers openAPI 3, even Webflux, OAuth 2. It's only a question of time when through contributions and feedback this can keep only getting better.

Conclusion

All said and done for a fairly recent API springdoc openAPI has achieved quite a lot very elegantly. Impressive. And it does cover most of the important scenarios thoroughly.

We achieved our objective of realizing FQNs or fully qualified names for java classes and also saw how to integrate the documentation with global exception handling leveraging Controller Advice.

Troubleshooting Tips

  • Ensure prerequisites.
  • If using the Eclipse IDE, we might need to do a Maven update on the project after creating all the files.
  • In the Swagger UI, if you are unable to access the “Schema” definitions link, it might be because you need to come out of the “try it out “ mode. Click on one or two Cancel buttons that might be visible.
  • Ensure you use http://localhost:8080/swagger-ui.html for this tutorial.


Source Code is here: https://github.com/teq-niq/sample/tree/springdoc-openapi-doingmore.
Git Clone URL: https://github.com/teq-niq/sample.git.
Branch: springdoc-openapi-doingmore.

Topics:
java ,openapi ,rest api ,spring boot ,swagger ,tutorial

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}