Description
Describe the bug
We have a controller that returns a time.Duration. This means the generated swagger specification shows the 200 response for the controller as having an object schema. We want to customize this response so that the schema is a string instead.
We wanted to use the OperationCustomizer to handle this:
Operation customize(Operation operation, HandlerMethod handlerMethod)
Our plan was to get the return type of the controller like so handlerMethod.getReturnType().getParameterType()
and if it was of a java.time.Duration, get the 200 response schema and set it to a string. The problem is that default API responses are not yet added so operation.getResponses()
returns null.
We understand that we can place ApiResponse annotation on the controller to override this however we want to avoid doing that because its a cross cutting concern and we don't to add code to the controller to generate a valid swagger spec.
To Reproduce
Steps to reproduce the behavior:
-
What version of spring-boot you are using?
2.2.6 -
What modules and versions of springdoc-openapi are you using?
group: 'org.springdoc', name: 'springdoc-openapi-ui', version: '1.3.2'
- What is the actual and the expected result using OpenAPI Description (yml or json)?
The actual response is this:
description: default response
content:
application/json:
schema:
type: object
properties:
seconds:
type: integer
format: int64
negative:
type: boolean
zero:
type: boolean
units:
type: array
items:
type: object
properties:
duration:
type: string
properties: {}
dateBased:
type: boolean
timeBased:
type: boolean
durationEstimated:
type: boolean
nano:
type: integer
format: int32
The expected response is this:
'200':
description: ok
content:
application/json:
schema:
type: string
- Provide with a sample code (HelloController) or Test that reproduces the problem.
I can't share the actual code but this is a close approximation. The key piece is the return type being Duration in the controller.
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.media.Content;
import io.swagger.v3.oas.models.media.Schema;
import org.springdoc.core.customizers.OperationCustomizer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.method.HandlerMethod;
import java.time.Duration;
import java.util.Map;
@SpringBootApplication
class Scratch {
public static void main(String[] args) {
SpringApplication.run(Scratch.class, args);
}
@Bean
public OperationCustomizer convertOperationsToString() {
return new JavaTimeOperationCustomizer();
}
private static class JavaTimeOperationCustomizer implements OperationCustomizer {
@Override
public Operation customize(Operation operation, HandlerMethod handlerMethod) {
if (handlerMethod.getReturnType().getParameterType().isAssignableFrom(Duration.class)) {
for (Map.Entry<String, io.swagger.v3.oas.models.responses.ApiResponse> entry: operation.getResponses().entrySet()) {
io.swagger.v3.oas.models.responses.ApiResponse response = entry.getValue();
Content content = response.getContent();
if (content.containsKey(MediaType.APPLICATION_JSON_VALUE)) {
Schema schema = content.get(MediaType.APPLICATION_JSON_VALUE).getSchema();
schema.getProperties().clear();
schema.setType("string");
}
}
}
return operation;
}
}
}
@RestController
class ScratchController {
@GetMapping(value = "/api/v2/timeout",
consumes = { MediaType.ALL_VALUE },
produces = { MediaType.APPLICATION_JSON_VALUE })
public Duration timeouts() {
return Duration.ofSeconds(5);
}
}
Expected behavior
operation.getResponses() is non-null / non-empty when the OperationCustomizer is executed