how to customize the swagger UI/OpenApi in spring boot 3 without make my controller look clumsy - swagger-ui

I would like to customize the swaggerUI with some custom messages. And i am able to create custom messages as below
#ApiOperation(value = "Add a new car to the inventory",
nickname = "addCar",
response = String.class,
responseContainer = "String")
#ApiResponses(value = {
#ApiResponse(code = 200, message = "Car Added Successfully",
response = String.class, responseContainer = "String"),
#ApiResponse(code = 405, message = "Invalid input") })
But it makes my controller looks really clumsy. Is there any way to customize it without make the controller so noisy.
Thanks

Related

How to use annotations to create OpenAPI (Swagger) documentation on Grails 4

We are creating API documentation for an existing Grails 4 App. We are having difficulties in understanding how to use Swagger annotations.
Let's assume the following Controller:
class IntegratorController {
def maintenanceService
def saveMaintenance() {
def message = 'success'
def status = '200'
try {
def maintenanceJson = request.JSON.maintenances
def ret=maintenanceService.processMaintenanceJSON(maintenanceJson)
} catch (Exception e) {
log.error("Error to process restricions", e)
message = 'error : ${e.getMessage()}'
status = '500'
}
def result = ['message':message]
render(status: status, contentType: "application/json", text: result as JSON)
}
}
This controller expects you to send a request JSON like this example:
{ "job":42,
"maintenances": [
{"idPort":42, "idMaintenance":42, "shipName":"myship01", "obs":"asap"},
{"idPort":43, "idMaintenance":43, "shipName":"myship02", "obs":"asap"}]}
A basic annotation will be this:
#Controller("/")
class IntegratorController {
def maintenanceService
#Post(uri="/saveMaintenance", produces = MediaType.APPLICATION_JSON)
#Consumes(MediaType.APPLICATION_JSON)
#Operation(summary = "Create one or more ship maintenance")
#ApiResponse(responseCode = "500", description = "If internal service throws an Exception")
def saveMaintenance() {
def message = 'success'
def status = '200'
try {
def maintenanceJson = request.JSON.maintenances
def savedMaintenances=maintenanceService.processMaintenanceJSON(maintenanceJson)
} catch (Exception e) {
log.error("Error to process restricions", e)
message = 'error : ${e.getMessage()}'
status = '500'
}
def result = ['message':message]
render(status: status, contentType: "application/json", text: result as JSON)
}
}
Where and how to annotate the request JSON sent in the post operation?
Thank you!
The request object is "scoped" by Grails. So you need to use #RequestBody annotation to declare what it is outside the method declaration. You also need to create classes to describe what it is because the JSON deserialization is loosely typed.
This is an example:
#Post(uri="/saveMaintenance", produces = MediaType.APPLICATION_JSON)
#Operation(summary = "Summary here",
description = "Description here",
requestBody = #RequestBody(description = "Inside Operation"), tags = ["IntegratorWebController"])
#RequestBody(description = "Description here", required = true,
content = #Content(schema = #Schema(implementation = YourRequestDAO.class, anyOf = [YourRequestDAO.class, YourRequestDAODependency.class])))
#ApiResponses(value=[
#ApiResponse(responseCode="200", description = "Return status=OK in success", content = #Content(mediaType = "application/json", schema = #Schema(implementation = YourResponseDAO.class))),
#ApiResponse(responseCode="404", description = "Return status=BAD_REQUEST if you mess up", content = #Content(mediaType = "application/json", schema = #Schema(implementation = YourResponseDAO.class)))])
def saveOrUpdateActivity(){
(...)
Well Swagger and OpenAPI are 'schemas' that are preloaded at runtime to build the call structure; GraphQL also has a schema as well to load its call structure.
I did a video on it here to help you understand how this works: https://youtu.be/AJJVnwULbbc
The way Grails did this prior to 4.0 was with plugins like the 'swagger plugin' or with BeAPI plugin (which I maintain).
I don't see a supported plugin in 4.0 so I don't see how they are doing this now.

Reusability of #ApiResponse Annotation in Swagger

I am using Swagger annotations to document API in non-spring context.
I find that response documentation for 400, 401 and 404 is reusable.
Since it takes around 8 lines to document each response code as shown below.
#Operation(
summary = "getDetails",
description = "someDescription",
responses = {
#ApiResponse(
responseCode = "200",
description = "Found",
content = #Content(mediaType = "application/json",
schema = #Schema(
name = "Response for success")
)
),
#ApiResponse(
responseCode = "400",
description = "Validation failure on inputs.",
content = #Content(
mediaType = "application/json",
schema = #Schema(
name = "Response For Bad Request")
)
),
#ApiResponse(
responseCode = "404",
description = "Not found",
content = #Content(
mediaType = "application/json",
schema = #Schema(
name = "Response For Not Found Request")
)
),
#ApiResponse(
responseCode = "401",
description = "Unauthorized",
content = #Content(
mediaType = "application/json",
schema = #Schema(
name = "Response For Unauthorized")
)
)
}
)
I intend to prevent bloating the lines for each reusable API response.
I would prefer something like below
#Operation(
summary = "getDetails",
description = "someDescription",
responses = {
#ApiResponse(
responseCode = "200",
description = "Found",
content = #Content(mediaType = "application/json",
schema = #Schema(
name = "Response for success")
)
),
SomeStaticClass.getBadResponseDesc(),
SomeStaticClass.getUnauthorizedResponseDesc()
}
Is there any way to achieve this in java 8?
Yes, use globalResponseMessage on the Docket object. See point 22 on the springfox documentation for an example of their use. I used this approach to remove a lot of #ApiResponse annotations from my code.

How to make the input paramter option in Swagger while calling Rest Endpoint

I already went through: How to define an optional parameter in path using swagger.
I've this endpoint:
#ApiOperation(value = "Retrieve Student Data By firstName Or lastName Or middleName",nickname = "Find Student Data")
#ApiResponses(value = { #ApiResponse(code = 200, message = "Successfully Retrieved Student Data"),
#ApiResponse(code = 404, message = "No data found !!!") })
#GetMapping(path = "/firstName/{firstName}/lastName/{lastName}/middleName/{middleName}")
public GetStudentDataResponse getStudentData(#PathVariable(required = false) String firstName, #PathVariable(required = false) String lastName,#PathVariable(required = false) String middleName) {
return service.getStudentData(firstName,lastName,middleName);
}
When I hit the rest endpoint and pass firstName only, Swagger is complaining about required parameter. How can we disabled it ?
Note: I really don't want to create another endpoint just to create / for the sake of to make it working via swagger.
You need to use #RequestParam instead of #PathVariable. Then it allows you to make the Parameters optional.

Create common component with Swagger annotations

I am using swagger-annotation for my spring-boot project.
I would like to return a common response code contract for each resource of my controller.
In the doc : https://github.com/swagger-api/swagger-core/wiki/annotations#apiresponses-apiresponse
they talk about #ApiResponses but I cannot put the annotation at the class level.
Here is what I did:
#Api(value = "Title",
description = "What this controller is about"
)
#ApiResponses(value = {
#ApiResponse(code = 400, message = "Bad stuff from the client"),
#ApiResponse(code = 404, message = "Item not found") }
)
public class FooBarController {
...
}
But the problem is that 400 - Bad stuff from the client and 404 - Item not found are never shown in the generated doc.
In the official doc of swagger I have seen this section: https://swagger.io/docs/specification/describing-responses/#reuse
Question: How can I create a kind of "reusable component" with java annotations ?
Thanks
According to the documentation, you can do this at the Docket level.
.useDefaultResponseMessages(false)
.globalResponseMessage(RequestMethod.GET,
newArrayList(new ResponseMessageBuilder()
.code(400)
.message("Bad stuff from the client")
.build()))
https://springfox.github.io/springfox/docs/current/#springfox-spring-mvc-and-spring-boot
Update:
If you want to go the annotation route, you can create your own and place it on your controller.
#Documented
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.TYPE)
#ApiResponses(value = {
#ApiResponse(code = 400, message = "Bad stuff from the client"),
#ApiResponse(code = 404, message = "Item not found") }
)
public #interface GlobalApiReponses {
}
Then use it..
#Api(value = "Title",
description = "What this controller is about"
)
#GlobalApiReponses
public class FooBarController
A combination of approaches might be a good option as well.
The #Target(ElementType.TYPE) means you can apply this at the class level. You can do the same for methods by using the ElemenType.METHOD.

Springfox global response header

In my spring boot rest API, I'm sending back a unique request id header "x-request-id" for every response (irrespective of the method) for every endpoint. I can add this using something like this:
#ApiResponses(value = {
#ApiResponse(
code = 200,
message = "Successful status response",
responseHeaders = {
#ResponseHeader(
name = "x-request-id",
description = "auto generated unique request id",
response = String.class)})
})
This works fine and I can see it in the Swagger UI. However, doing this for every endpoint is a tedious + maintenance problem. I'm looking to do this globally but the Springfox documentation only shows about global response message using .globalResponseMessage option - I can't find anything for global response headers.
Ended up creating an annotation to handle this:
package com.abc.xyz.api.docs.annotations;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import io.swagger.annotations.ResponseHeader;
import com.abc.xyz.api.constants.ApiConstants;
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.RUNTIME)
#Documented
#Inherited
#ApiResponses(value = {
#ApiResponse(
code = 200,
message = "Successful status response",
responseHeaders = {
#ResponseHeader(
name = ApiConstants.REQUESTIDHEADER,
description = ApiConstants.REQUESTIDDESCRIPTION,
response = String.class)}),
#ApiResponse(
code = 401,
message = "Successful status response",
responseHeaders = {
#ResponseHeader(
name = ApiConstants.REQUESTIDHEADER,
description = ApiConstants.REQUESTIDDESCRIPTION,
response = String.class)}),
#ApiResponse(
code = 403,
message = "Successful status response",
responseHeaders = {
#ResponseHeader(
name = ApiConstants.REQUESTIDHEADER,
description = ApiConstants.REQUESTIDDESCRIPTION,
response = String.class)}),
#ApiResponse(
code = 404,
message = "Successful status response",
responseHeaders = {
#ResponseHeader(
name = ApiConstants.REQUESTIDHEADER,
description = ApiConstants.REQUESTIDDESCRIPTION,
response = String.class)}),
}
)
public #interface RequestIdMethod {};
With this, I can add this as a marker annotation in front of my methods:
#RequestMapping(value = "/heartbeat", method = RequestMethod.GET)
#RequestIdMethod
public Heartbeat checkHeartbeat() {
return new Heartbeat(status);
}
It is not great because I need to repeat the entire #ApiResponse annotation block for every http return code (obviously there could be other return codes but I only covered the default codes shown by Springfox). Would have been better if there was a way to parameterize the entire #ApiResponse block.
I know I'm late to the party here, but I did find a way to globally add a header to every response using reflection (might not be required but turned out to be the easiest way for me to get EVERY response. You can also check for all ApiResponses annotations but some were added implicitly and therefore left out with that approach).
#Component
#Order(SwaggerPluginSupport.SWAGGER_PLUGIN_ORDER + 10)
public class RequestIdResponseHeaderPlugin implements OperationBuilderPlugin {
#Override
public boolean supports(DocumentationType documentationType) {
return true;
}
#Override
public void apply(OperationContext operationContext) {
try {
// we use reflection here since the operationBuilder.build() method would lead to different operation ids
// and we only want to access the private field 'responseMessages' to add the request-id header to it
Field f = operationContext.operationBuilder().getClass().getDeclaredField("responseMessages");
f.setAccessible(true);
Set<ResponseMessage> responseMessages = (Set<ResponseMessage>) f.get(operationContext.operationBuilder());
responseMessages.forEach(message -> {
int code = message.getCode();
Map<String, Header> map = new HashMap<>();
map.put("my-header-name", new Header(null, null, new ModelRef("string")));
ResponseMessage responseMessage = new ResponseMessageBuilder().code(code).headersWithDescription(map).build();
operationContext.operationBuilder().responseMessages(Collections.singleton(responseMessage));
});
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
}
Found this way after looking into the method responseMessages() of the operation-builder. It internally merges response-headers based on the status-code and the logic itself will simply add headers to existing response-headers.
Hope it helps someone since it does not require you to annotate every single endpoint.
I updated my Docket configuration to include the Global header on every API. Hope this helps.
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(new ApiInfoBuilder()
.contact(new Contact("My Support", null, "My Email"))
.description("My Description")
.licenseUrl("My License")
.title("My Title")
.termsOfServiceUrl("My Terms and Conditions")
.version("My Version")
.build())
.globalOperationParameters(Collections.singletonList(new ParameterBuilder()
.name("x-request-id")
.modelRef(new ModelRef("string"))
.parameterType("header")
.required(false)
.build()))
.select()
.paths(PathSelectors.regex("/user*))
.build()
.directModelSubstitute(LocalDate.class, String.class)
.directModelSubstitute(LocalDateTime.class, String.class);

Resources