Spring Boot | Using SWAGGER at maximum — Docket advanced details
Paladuta Stefan
Posted on July 8, 2023
The main article that bundles all together and redirect to different features that you can implement in swagger UI is present here: https://medium.com/@stefan.paladuta17/spring-boot-using-swagger-at-maximum-9828ae9fb9d0
In this article I am going to show how to populate in more detail the Docket object used to generate the SWAGGER-UI that we all appreciate and use.
Chapter: Producing and Consuming
Chapter: Adding extra models
Chapter: Global request parameters default
Chapter: Global response default
Chapter 1 Producing and Consuming
Producing and consuming actions are two details that I personally appreciate when I see them in projects. It’s very nice and helpful when you see a documentation regarding an API that tells you as consumer what form of response it can give you: json, xml, custom response, etc. and what form of information you need to send, thus making the experience more valuable.
To set what is the API producing and consuming we can see the methods .produces() and .consumes() on the Docket object. These two methods expect from you, a collection of type Set of what forms will the application accept/offer. In case you are wondering why Set collection, you just need to simply remember: “A Set is a Collection that cannot contain duplicate elements.” which with other words means the UI should render only unique forms thus a Set is perfect for this case.
A basic example of the entire class with the Docket object populated for consuming:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
@Configuration
public class SpringFoxConfig {
@Bean
public Docket getDocketInstance() {
final Set<String> produces = new HashSet<String>();
produces.add(MediaType.APPLICATION_JSON_VALUE);
produces.add(MediaType.APPLICATION_XML_VALUE);
produces.add("Stefan's custom mapping value");
return new Docket(DocumentationType.SWAGGER_2)
.produces(produces)
.select()
.apis(RequestHandlerSelectors.basePackage("ro.stefan.controller"))
.paths(PathSelectors.any())
.build();
}
}
NOTE: produces.add(“Stefan’s custom mapping value”); it’s here just to prove the concept that it’s just a string applied on that collection + you shouldn’t normally go outside of the standard defined MIME formats.
Why do this ?
Because as mentioned already an API can accept and return data in different formats (json, xml, etc.) so we can useconsumes and produces to specify the MIME types understood by your API. To set the consumes and produces type is easy and just represents an array of MIME type.
This modification as presented in the examples are done globally so that means that all controllers from that spring boot project will render this but you can override this value at API level (method from the controller) if you have exceptions.
Important note: “Note that consumes only affects operations with a request body, such as POST, PUT and PATCH.” — swagger.io
What MIME type we can use ?
- application/json
- application/xml
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain; charset=utf-8
- text/html
- application/pdf
- image/png etc...
Chapter 2 — Adding extra models
“A key feature offered by Springfox that was very appealing for reverse-engineering/retrofitting an existing Spring Boot microservice to generate an OAS was the ability to ad-hoc pick up POJOs and have a schema generated for it.” — https://github.com/springdoc/springdoc-openapi/issues/415.
I put the following quote because it expresses very simple the current chapter. Adding extra models in the SWAGGER-UI is actually a thing that I saw being used very often by other teams.
@Autowired
private TypeResolver typeResolver;
@Bean
public Docket getDocketInstanceEx1() {
final Set<String> produces = new HashSet<String>();
produces.add(MediaType.APPLICATION_JSON_VALUE);
produces.add(MediaType.APPLICATION_XML_VALUE);
return new Docket(DocumentationType.SWAGGER_2)
.produces(produces)
.consumes(produces)
.additionalModels(typeResolver.resolve(Author.class))
.select()
.apis(RequestHandlerSelectors.basePackage("ro.stefan.controller"))
.paths(PathSelectors.any())
.build();
}
NOTE: Personally I never had a reason to do this but only because I didn’t have a reason or opportunity it doesn’t mean it’s useless I just never had the chance.
Why would you this ?
Based on my reason I just found the following:
This issue: https://github.com/springdoc/springdoc-openapi/issues/415 that explain that the desire to add extra models is to generate a bigger open API json. I wanted to simply generate the Open API spec yaml/json as a baseline with the existing Spring Boot controllers, but then manually write up summaries, descriptions, response types, and return types in the yaml itself. My primary use case here is for large existing microservices that weren’t built-out with an Open API or API driven development mindset. (Also, the return types of our controllers are almost always Strings because we serialize manually with GSON as opposed to Jackson w/ Spring, but I think swagger-core provides annotations that resolve this anyway)
Other teams/projects do this because they have old systems where the controller returns a generic String that represents an object in json format.
Chapter 3 — Global request parameters default
The method: .globalRequestParameters() is one of the most useful features we have in Docket. It allows you to add for all requests (all endpoints of the spring boot project) written in all controllers a parameter that can be:
- Query parameter
- Header attribute
- Cookie
- Path
- Form
- Body
- Form Data
Values extracted from the springfox.documentation.service.ParameterType enum.
Example with header attribute:
package ro.stefan.config;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import com.fasterxml.classmate.TypeResolver;
import ro.stefan.dto.Author;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.builders.RequestParameterBuilder;
import springfox.documentation.schema.ScalarType;
import springfox.documentation.service.ParameterType;
import springfox.documentation.service.RequestParameter;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
@Configuration
public class SpringFoxConfig {
@Autowired
private TypeResolver typeResolver;
@Bean
public Docket getDocketInstanceEx1() {
final Set<String> produces = new HashSet<String>();
produces.add(MediaType.APPLICATION_JSON_VALUE);
produces.add(MediaType.APPLICATION_XML_VALUE);
List<RequestParameter> listRequestParamters = new ArrayList<RequestParameter>();
RequestParameter requestParamterToken = new RequestParameterBuilder()
.name("ACCESS_TOKEN")
.required(true)
.query(q -> q.model(modelSpecificationBuilder -> modelSpecificationBuilder.scalarModel(ScalarType.STRING)))
.in(ParameterType.HEADER)
.build();
listRequestParamters.add(requestParamterToken );
return new Docket(DocumentationType.SWAGGER_2)
.produces(produces)
.consumes(produces)
.globalRequestParameters(listRequestParamters)
.additionalModels(typeResolver.resolve(Author.class))
.select()
.apis(RequestHandlerSelectors.basePackage("ro.stefan.controller"))
.paths(PathSelectors.any())
.build();
}
}
//----------------------------------------------------------------------------------------------------------------------
import java.util.ArrayList;
import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import ro.stefan.dto.Book;
@RestController
public class BookController {
@GetMapping("/books")
public List<Book> getBooks(){
return new ArrayList<Book>();
}
}
so we can see now in the SWAGGER-UI the header attribute ACCESS_TOKEN is mandatory for the endpoint to be called. Thus avoiding to write code in Controller that is just used on an interceptor or filter.
Example with query parameter:
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import com.fasterxml.classmate.TypeResolver;
import ro.stefan.dto.Author;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.builders.RequestParameterBuilder;
import springfox.documentation.schema.ScalarType;
import springfox.documentation.service.ParameterType;
import springfox.documentation.service.RequestParameter;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
@Configuration
public class SpringFoxConfig {
@Autowired
private TypeResolver typeResolver;
@Bean
public Docket getDocketInstanceEx1() {
final Set<String> produces = new HashSet<String>();
produces.add(MediaType.APPLICATION_JSON_VALUE);
produces.add(MediaType.APPLICATION_XML_VALUE);
List<RequestParameter> listRequestParamters = new ArrayList<RequestParameter>();
RequestParameter limitParameter = new RequestParameterBuilder()
.name("limit")
.required(false)
.query(q -> q.model(modelSpecificationBuilder -> modelSpecificationBuilder.scalarModel(ScalarType.STRING)))
.in(ParameterType.QUERY)
.build();
RequestParameter pagesParameter = new RequestParameterBuilder()
.name("pages")
.required(false)
.query(q -> q.model(modelSpecificationBuilder -> modelSpecificationBuilder.scalarModel(ScalarType.STRING)))
.in(ParameterType.QUERY)
.build();
listRequestParamters.add(limitParameter);
listRequestParamters.add(pagesParameter);
return new Docket(DocumentationType.SWAGGER_2)
.produces(produces)
.consumes(produces)
.globalRequestParameters(listRequestParamters)
.additionalModels(typeResolver.resolve(Author.class))
.select()
.apis(RequestHandlerSelectors.basePackage("ro.stefan.controller"))
.paths(PathSelectors.any())
.build();
}
}
So… we can see that the line of code that dictates the types is .in(ParameterType.QUERY)
Example with cookie parameter:
If you want to see the entire code and example just visit my repository on GitHub and written at the bottom of this article.
Now as you can imagine the possibilities are big.
Why would you need this ?
Project’s where you have interceptors or filters and per each request sent by the client and you want to extract an information like a access token (jwt, etc.). It’s clear that you don’t write the token variable into the controller because the controller will never use the variable so instead of having a required variable written in Controller that will never be used there, you can avoid writing it and put it directly into the Docket property if the variable is mandatory for all request of your microservices.
Chapter 4 — Global response default
Swagger API’s allow you as developer to override the default response messages of different HTTP requests by telling it (swagger) the to stop using default responses .useDefaultResponseMessages(false) and starting to construct your own cases with the method: .globalResponses()
NOTE: Remember all of this methods are applied at Docket level.
So before overring it in swagger UI we will see that in case of 500 and 403 error messages we would get:
After applying the following code:
import java.util.ArrayList;
import java.util.List;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.builders.ResponseBuilder;
import springfox.documentation.service.Response;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
@Configuration
public class SpringFoxConfig {
@Bean
public Docket getDocketInstanceEx1() throws JsonProcessingException {
List<Response> listResponses = new ArrayList<Response>();
class ServerError{
private String serverIp;
private String serverName;
private String errorMsg;
private String errorStack;
public String getServerIp() {
return serverIp;
}
public void setServerIp(String serverIp) {
this.serverIp = serverIp;
}
public String getServerName() {
return serverName;
}
public void setServerName(String serverName) {
this.serverName = serverName;
}
public String getErrorMsg() {
return errorMsg;
}
public void setErrorMsg(String errorMsg) {
this.errorMsg = errorMsg;
}
public String getErrorStack() {
return errorStack;
}
public void setErrorStack(String errorStack) {
this.errorStack = errorStack;
}
}
ObjectMapper objMapper = new ObjectMapper();
listResponses.add(new ResponseBuilder().code("500").description(objMapper.writeValueAsString(new ServerError())).build());
listResponses.add(new ResponseBuilder().code("403").description("Forbidden!!!!!").build());
return new Docket(DocumentationType.SWAGGER_2)
.useDefaultResponseMessages(false)
.globalResponses(HttpMethod.GET,listResponses)
.select()
.apis(RequestHandlerSelectors.basePackage("ro.stefan.controller"))
.paths(PathSelectors.any())
.build();
}
}
we can see:
So as you can imagine again this feature brings a ton of value to you as a user that offers API’s and value to a consumer that will bother you less because you documentation just got more precious.
Why would you need this ?
Because in an enterprise environment as much as I hate saying we must understand that we aren’t a team of 10 or 20 or 30 people we are a team over 300+ developers on different projects that are part of the same ecosystem so naturally if you let everyone define it’s own style the system wouldn’t be so optimal. So based on what I said this feature is used when the company architect say “All web services should have this standard response in case of failure (server) or non-auth.” thus reducing the complexity of the system and putting down a LAW (yes, I hate this but makes sense if you think about it at cold).
Conclusion: There are a lot of features that Docket object from swagger offers but please always take in consideration if the feature is old or deprecated and try to find something that is more up to date. Even this article presents things using the SWAGGER_2 version when now the hype is version 3.
If you want my code you can find it here: https://github.com/Kanames/SpringBoot-SwaggerExamples
If you liked the article please take a minute to offer me a clap 👏 or even buy me a coffee https://www.buymeacoffee.com/stefansplace (;
Original post: https://medium.com/@stefan.paladuta17/spring-boot-using-swagger-at-maximum-docket-advanced-details-6b72b334be21
Posted on July 8, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.