Description
Describe the bug
All our ressources require auth and have their error response follow "Problem Details" RFC.
Consequently HTTP 401 and 403 responses are commons.
This is an example of API describing these commons API responses:
@RestController
@ApiResponses(value = {
@ApiResponse(responseCode = "401", description = "Invalid authentication.", content = {@Content(schema = @Schema(implementation = Problem.class), mediaType = APPLICATION_PROBLEM_JSON_VALUE)}),
@ApiResponse(responseCode = "401", description = "Invalid authentication.",content = {@Content(schema = @Schema(implementation = Problem.class), mediaType = APPLICATION_PROBLEM_JSON_VALUE)}),
@ApiResponse(responseCode = "403", description = "Missing authorities.",content = {@Content(schema = @Schema(implementation = Problem.class), mediaType = APPLICATION_PROBLEM_JSON_VALUE)}) })
public class HelloController<T> {
private static final Collection<String> CURRENCIES = new ArrayList<>();
static {
CURRENCIES.add("EUR");
CURRENCIES.add("USD");
}
@GetMapping
@Operation(description = "Get all currencies", summary = "getAllCurrencies")
@ApiResponses(value = { @ApiResponse(responseCode = "200", description = "All currencies returned") })
public ResponseEntity<Collection<String>> getAllCurrencies() {
return ok(CURRENCIES);
}
}
From that we have a correctly generated swagger:
{
"openapi":"3.0.1",
"info":{
"title":"OpenAPI definition",
"version":"v0"
},
"servers":[
{
"url":"http://localhost",
"description":"Generated server url"
}
],
"paths":{
"/":{
"get":{
"tags":[
"hello-controller"
],
"summary":"getAllCurrencies",
"description":"Get all currencies",
"operationId":"getAllCurrencies",
"responses":{
"401":{
"description":"Invalid authentication.",
"content":{
"application/problem+json":{
"schema":{
"$ref":"#/components/schemas/Problem"
}
}
}
},
"403":{
"description":"Missing authorities.",
"content":{
"application/problem+json":{
"schema":{
"$ref":"#/components/schemas/Problem"
}
}
}
},
"200":{
"description":"All currencies returned",
"content":{
"*/*":{
"schema":{
"type":"array",
"items":{
"type":"string"
}
}
}
}
}
}
}
}
},
"components":{
"schemas":{
"Problem":{
"type":"object",
"properties":{
"instance":{
"type":"string",
"format":"uri"
},
"type":{
"type":"string",
"format":"uri"
},
"parameters":{
"type":"object",
"additionalProperties":{
"type":"object"
}
},
"status":{
"type":"integer",
"format":"int32"
},
"title":{
"type":"string"
},
"detail":{
"type":"string"
}
}
}
}
}
}
To help developer, we provide a generic OpenApiCustomiser to provide these standards response:
public class ResponseRegistrationCustomizer implements OpenApiCustomiser {
private final List<Map.Entry<String, ApiResponse>> responsesToRegister;
public ResponseRegistrationCustomizer(@NonNull List<Map.Entry<String, ApiResponse>> responsesToRegister) {
this.responsesToRegister = responsesToRegister;
}
@Override
public void customise(OpenAPI openApi) {
responsesToRegister.forEach(entry -> openApi.getComponents().addResponses(entry.getKey(), entry.getValue()));
log.debug("Registered {} responses in OpenAPI Specification", responsesToRegister.size());
}
}
and an uncoupled configuration to provide these communs responses:
@Configuration
public class SecurityProblemResponsesConfiguration {
private static final String HTTP_401_NO_TOKEN = "http401NoToken";
private static final String HTTP_401_BAD_TOKEN = "http401BadToken";
private static final String HTTP_403 = "http403";
public static final String UNAUTHORIZED_401_NO_TOKEN_RESPONSE_REF = "#/components/responses/" + HTTP_401_NO_TOKEN;
public static final String UNAUTHORIZED_401_BAD_TOKEN_RESPONSE_REF = "#/components/responses/" + HTTP_401_BAD_TOKEN;
public static final String FORBIDDEN_403_RESPONSE_REF = "#/components/responses/" + HTTP_403;
@Bean
public Map.Entry<String, ApiResponse> http401NoTokenResponse() throws IOException {
return simpleResponse(HTTP_401_NO_TOKEN, "Unauthorized", "Invalid authentication.");
}
@Bean
public Map.Entry<String, ApiResponse> http401BadTokenResponse() throws IOException {
return simpleResponse(HTTP_401_BAD_TOKEN, "Unauthorized", "Invalid authentication.");
}
@Bean
public Map.Entry<String, ApiResponse> http403Example() throws IOException {
return simpleResponse(HTTP_403, "Forbidden", "Missing authorities.";
}
@Bean
public Map.Entry<String, ApiResponse> http500Response() throws IOException {
return simpleResponse(HTTP_500, "Internal Server Error", "HTTP 500 JSON Body response example");
}
private Map.Entry<String, ApiResponse> simpleResponse(String code, String description) throws IOException {
ApiResponse response = new ApiResponse().description(description).content(new Content().addMediaType(
APPLICATION_PROBLEM_JSON_VALUE,
new MediaType()
.schema(new Schema<Problem>().$ref("#/components/schemas/Problem"))));
return new AbstractMap.SimpleEntry<>(code, response);
}
}
This help keep a REST controller more readable and formatable, especially when @ExampleObject are provided as static JSON file instead of having them directly in REST controller.
From these help classes, now developer can have following REST controller:
@RestController
@ApiResponses(value = {
@ApiResponse(responseCode = "401", ref = SecurityProblemResponsesConfiguration.UNAUTHORIZED_401_NO_TOKEN_RESPONSE_REF),
@ApiResponse(responseCode = "401", ref = SecurityProblemResponsesConfiguration.UNAUTHORIZED_401_BAD_TOKEN_RESPONSE_REF),
@ApiResponse(responseCode = "403", ref = SecurityProblemResponsesConfiguration.FORBIDDEN_403_RESPONSE_REF) })
public class HelloController<T> {
private static final Collection<String> CURRENCIES = new ArrayList<>();
static {
CURRENCIES.add("EUR");
CURRENCIES.add("USD");
}
@GetMapping
@Operation(description = "Get all currencies", summary = "getAllCurrencies")
@ApiResponses(value = { @ApiResponse(responseCode = "200", description = "All currencies returned") })
public ResponseEntity<Collection<String>> getAllCurrencies() {
return ok(CURRENCIES);
}
}
but the swagger generated becomes:
{
"openapi":"3.0.1",
"info":{
"title":"OpenAPI definition",
"version":"v0"
},
"servers":[
{
"url":"http://localhost",
"description":"Generated server url"
}
],
"paths":{
"/":{
"get":{
"tags":[
"hello-controller"
],
"summary":"getAllCurrencies",
"description":"Get all currencies",
"operationId":"getAllCurrencies",
"responses":{
"401":{
"$ref":"#/components/responses/http401BadToken"
},
"403":{
"$ref":"#/components/responses/http403"
},
"200":{
"description":"All currencies returned",
"content":{
"*/*":{
"schema":{
"type":"array",
"items":{
"type":"string"
}
}
}
}
}
}
}
}
},
"components":{
"responses":{
"http401NoToken":{
"description":"Invalid authentication.",
"content":{
"application/problem+json":{
"schema":{
"$ref":"#/components/schemas/Problem"
}
}
}
},
"http401BadToken":{
"description":"Invalid authentication.",
"content":{
"application/problem+json":{
"schema":{
"$ref":"#/components/schemas/Problem"
}
}
}
},
"http403":{
"description":"Missing authorities.",
"content":{
"application/problem+json":{
"schema":{
"$ref":"#/components/schemas/Problem"
}
}
}
}
}
}
}
without the "#/components/schemas/Problem".
It seems that schemas are analyzed when they exist in response annotations but not in ref.
Could you confirm me if it is a bug or it is responsibility of APIResponse builder to register this Problem schema among components?
Best Regards.