I'm using OpenApi 3 in my SpringBoot project in order to generate a Swagger html page.
The dependency in POM.xml :
<dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-ui</artifactId> <version>1.5.12</version> </dependency>
In the Controller class I've defined the following Annotations above the method.
@Operation(
summary = "Get a list of letters for a specific user",
description = "Get a list of letters for a specific user",
tags = {"letters"}
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "success", content = {@Content(
mediaType = "application/json",
array = @ArraySchema(schema = @Schema(implementation = LetterDTO.class)))}),
@ApiResponse(responseCode = "400", description = "BAD REQUEST"),
@ApiResponse(responseCode = "401", description = "UNAUTHORIZED"),
@ApiResponse(responseCode = "403", description = "Forbidden"),
@ApiResponse(responseCode = "404", description = "NOT_FOUND: Entity could not be found")}
)
@GetMapping(value = "letters/user/{userId}", produces = {"application/json"})
public List<LetterDTO> getLettersForUser(
...
)
The output of Swagger UI shows the correct response for code 200, which is a list of LetterDTO objects.
But the response for code 401 also show a list of LetterDTO objects. Al tough I didn't define any response object for code 401. I was expecting Swagger to generate the same response object like for code 400, which is a default return object containing the error code and a error message.
Why does Swagger take the same return object like the one defined for code 200 ? I was expecting that Swagger would generate the default return object. Is this a Bug in Swagger ?
I normally configure API responses like this:
@ApiResponse(responseCode = "200", description = "OK")
@ApiResponse(responseCode = "400", description = "Invalid request", content = @Content)
If no content
is specified, the return type of the respective Controller method is used. content = @Content
tells Swagger that there is no content in the response.
For @ApiGetOne
this is what Swagger would display (the screenshot is from a different DTO class):
For simplicity and reusability, I typically wrap these in reusable helper annotations, this way my endpoints don't have as many annotations and I don't need a ResponseEntity
in the controller, e.g.:
@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@RequestMapping(method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
@ApiResponse(responseCode = "200", description = "OK")
@ApiResponse(responseCode = "400", description = "Invalid request", content = @Content)
@ApiResponse(responseCode = "500", description = "Internal error", content = @Content)
public @interface ApiGet {
@AliasFor(annotation = RequestMapping.class)
String[] value() default {};
@AliasFor(annotation = RequestMapping.class)
String[] path() default {};
}
You can also extend these annotations with more API responses, e.g., to add a 404 for some endpoints, create another annotation that also has @ApiGet
on it:
@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@ApiGet
@ApiResponse(responseCode = "404", description = "Not found", content = @Content)
public @interface ApiGetOne {
@AliasFor(annotation = ApiGet.class)
String[] value() default {};
@AliasFor(annotation = ApiGet.class)
String[] path() default {};
}
and finally, use them on any endpoint (using Java 17):
public record HelloWorldDto(String recipientName) {
public String getMessage() {
return "Hello, %s".formatted(recipientName);
}
}
public record ErrorDto(String message) {
}
@RestController
@RequestMapping("api/test")
@Tag(name = "Demo", description = "Endpoints for testing")
public class DemoController {
...
@ApiGet("/hello")
public HelloWorldDto sayHello() {
return new HelloWorldDto("stranger");
}
@ApiGetOne("/hello/{id}")
public HelloWorldDto sayHelloWithParam(@PathVariable int id) {
final var person = myPersonRepo.getById(id); // might throw a NotFoundException which is mapped to 404 status code
return new HelloWorldDto(person.name());
}
}
Mapping exceptions to custom error responses:
@ControllerAdvice
public class ErrorHandler {
private static final Logger log = LoggerFactory.getLogger(ErrorHandler.class);
@ExceptionHandler
public ResponseEntity<ErrorDto> handle(Exception exception) {
log.error("Internal server error occurred", exception);
return response(HttpStatus.INTERNAL_SERVER_ERROR, "Unknown error occurred.");
}
@ExceptionHandler
public ResponseEntity<ErrorDto> handle(NotFoundException exception) {
return response(HttpStatus.NOT_FOUND, exception.getMessage());
}
private ResponseEntity<ErrorDto> response(HttpStatus status, String message) {
return ResponseEntity
.status(status)
.body(new ErrorDto(message));
}
}
I like this setup a lot because
ResponseEntity
in controller methods@ControllerAdvice
serves as a central point of reusable error handlingUpdate 2022/04/20
Just had to fix a bug where we have an endpoint that returns images instead of JSON. In this case, to prevent HttpMessageNotWritableException: No converter for [class ErrorDto] with preset Content-Type 'image/jpeg'
, you need to check the Accept
header of the request like so (using a header as fallback):
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorDto> handle(final Exception exception, final WebRequest webRequest) {
return createResponse(HttpStatus.INTERNAL_SERVER_ERROR, "Some error", webRequest);
}
protected ResponseEntity<ErrorDto> createResponse(final HttpStatus httpStatus,
final String message,
final WebRequest webRequest) {
final var accepts = webRequest.getHeader(HttpHeaders.ACCEPT);
if (!MediaType.APPLICATION_JSON_VALUE.equals(accepts)) {
return ResponseEntity.status(httpStatus)
.header("my-error", message)
.build();
}
return ResponseEntity
.status(status)
.body(new ErrorDto(message));
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With