Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java SpringBoot OpenApi @ApiResponse shows wrong return object

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.

enter image description here

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.

enter image description here

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 ?

enter image description here

like image 379
user2023141 Avatar asked Oct 12 '25 10:10

user2023141


1 Answers

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):

enter image description here

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

  • I end up with a handful of reusable annotations that are sufficient for typical CRUD endpoints
  • I don't need to build ResponseEntity in controller methods
  • the @ControllerAdvice serves as a central point of reusable error handling
  • all of which keeps my controllers/endpoints clean and simple
  • and that, in turn, keeps testing simple

Update 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));
}
like image 77
sschmid Avatar answered Oct 15 '25 00:10

sschmid