spring-bootopenapispringdoc

Spring-Boot OpenAPI - @RestControllerAdvice not limited to methods throwing the Exception


I need to document my SpringBoot APIs and their possible exceptions with OpenAPI, and I am using SpringDoc-OpenAPI https://springdoc.org/.

To handle the NotFound cases, I created this exception class:

import org.springframework.http.HttpStatus;
import org.springframework.web.server.ResponseStatusException;

public class NotFoundException extends ResponseStatusException {
    public NotFoundException() {
        super(HttpStatus.NOT_FOUND);
    }
}

and this @RestControllerAdvice

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalControllerExceptionHandler {
    @ExceptionHandler(NotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ResponseEntity<String> handleNotFoundException(RuntimeException ex) {
        return new ResponseEntity<>(ex.getMessage(), HttpStatus.NOT_FOUND);
    }
}

The problem I am facing is that the generated OpenAPI yaml file has

  responses:
    "404":
      description: Not Found
      content:
        '*/*':
          schema:
            type: string

for all @RestController endpoints, instead of only for the methods with throws NotFoundException.

How can I limit the @ControllerAdvice (or the OpenAPI), to generate the 404 Response documentation only for methods with the throwing signature?

Do I need to use something else other than the @RestControllerAdvice? I would like to avoid having to annotate every single method.


Solution

  • A possible solution is to:

    1. Make the @RestControllerAdvice @Hidden
    2. Provide an OperationCustomizer @Bean
    import io.swagger.v3.oas.annotations.Hidden;
    import it.eng.cysec.ot.risk.assessment.api.exceptions.NotFoundException;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.ResponseStatus;
    import org.springframework.web.bind.annotation.RestControllerAdvice;
    
    @Hidden
    @RestControllerAdvice
    public class GlobalControllerExceptionHandler {
        @ExceptionHandler(NotFoundException.class)
        @ResponseStatus(HttpStatus.NOT_FOUND)
        public ResponseEntity<String> handleNotFoundException(NotFoundException exception) {
            return new ResponseEntity<>(exception.getMessage(), HttpStatus.NOT_FOUND);
        }
    }
    
    import io.swagger.v3.oas.models.Operation;
    import io.swagger.v3.oas.models.media.Content;
    import io.swagger.v3.oas.models.media.MediaType;
    import io.swagger.v3.oas.models.media.StringSchema;
    import io.swagger.v3.oas.models.responses.ApiResponse;
    import io.swagger.v3.oas.models.responses.ApiResponses;
    import it.eng.cysec.ot.risk.assessment.api.exceptions.NotFoundException;
    import org.springdoc.core.customizers.OperationCustomizer;
    import org.springframework.web.method.HandlerMethod;
    
    import java.lang.reflect.Method;
    import java.util.Arrays;
    import java.util.List;
    
    public class OperationResponseCustomizer implements OperationCustomizer {
        public static final ApiResponse NOT_FOUND_API_RESPONSE;
    
    
        static {
            MediaType mediaType = new MediaType();
            mediaType.setSchema(new StringSchema());
    
            Content content = new Content();
            content.addMediaType("*/*", mediaType);
    
            NOT_FOUND_API_RESPONSE = new ApiResponse()
                    .description("Not Found")
                    .content(content);
        }
    
        /**
         * Customize operation.
         *
         * @param operation     input operation
         * @param handlerMethod original handler method
         * @return customized operation
         */
        @Override
        public Operation customize(Operation operation, HandlerMethod handlerMethod) {
            Method method = handlerMethod.getMethod();
            List<Class<?>> exceptions = Arrays.asList(method.getExceptionTypes());
    
            if(exceptions.contains(NotFoundException.class)){
                ApiResponses apiResponses = operation.getResponses();
                apiResponses.addApiResponse("404", NOT_FOUND_API_RESPONSE);
            }
    
            return operation;
        }
    }