javaspringspring-bootexceptionswagger-ui

Spring Boot Swagger throws "Failed to load API definition. Response status is 500 /v3/api-docs" after adding Exception Handlers


this is my first time asking a question here, so please bear with me if I miss anything.

I’ve created a simple Spring Boot banking project with the following REST APIs:

/create

/getAccount

/deposit

/withdraw

/deleteAccount

Everything works perfectly:

  1. The APIs function correctly and update the database as expected
  2. Swagger UI shows all endpoints as intended
  3. Postman tests also work without any issues

However, when I added custom exception handling using @ControllerAdvice, Swagger started throwing this error:

Failed to load API definition. Response status is 500 /v3/api-docs

Here’s the strange part: when I remove the exception classes, Swagger starts working again! So clearly, there’s something in the exception handling setup that’s causing this issue.

Could someone help me figure out what’s going wrong?

ErrorResponse class:

package com.bank.bankingApplication.exceptions;

import java.time.LocalDateTime;

public class ErrorResponse {
    private String message;
    private String timestamp;
    private String path;

    public ErrorResponse(String message, String path) {
        this.message = message;
        this.timestamp = LocalDateTime.now().toString();
        this.path = path;
    }

    // Getters and Setters

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public String getTimestamp() {
        return timestamp;
    }

    public void setTimestamp(String timestamp) {
        this.timestamp = timestamp;
    }

    public String getPath() {
        return path;
    }

    public void setPath(String path) {
        this.path = path;
    }
}

GlobalExceptionHandler class:

package com.bank.bankingApplication.exceptions;


import com.bank.bankingApplication.controller.BankController;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;


@ControllerAdvice
@RestControllerAdvice(assignableTypes = {BankController.class})
public class GlobalExceptionHandler {

//    @ExceptionHandler(Exception.class)
//    public ResponseEntity<ErrorResponse> handleAllExceptions(Exception ex, HttpServletRequest request) {
//        ErrorResponse error = new ErrorResponse(ex.getMessage(), request.getRequestURI());
//        return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
//    }

    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
    public ResponseEntity<ErrorResponse> handleTypeMismatch(MethodArgumentTypeMismatchException ex, HttpServletRequest request) {
        String msg = "Invalid type for parameter: " + ex.getName();
        ErrorResponse error = new ErrorResponse(msg, request.getRequestURI());
        return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<ErrorResponse> handleIllegalArgs(IllegalArgumentException ex, HttpServletRequest request) {
        ErrorResponse error = new ErrorResponse(ex.getMessage(), request.getRequestURI());
        return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
    }

    // You can add more specific ones like @ExceptionHandler(AccountNotFoundException.class) here.
}

Controller:

package com.bank.bankingApplication.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import com.bank.bankingApplication.model.Account;
import com.bank.bankingApplication.repository.AccountRepository;
import com.bank.bankingApplication.service.AccountService;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;

@RestController
@RequestMapping("/bank")
@Tag(name = "Bank Controller", description = "Handles operations related to bank accounts")
public class BankController {
    @Autowired
    private AccountService accountService;

    @Autowired
    private AccountRepository accountRepository;

    @Operation(
            summary = "Create a new bank account",
            description = "This endpoint is used to create a new bank account for a user."
    )
    @PostMapping("/create")
    public Account createAccount(@RequestBody Account account){
        return accountService.createAccount(account);
    }

    @Operation(
            summary = "Fetches the existing bank account",
            description = "This endpoint is used to fetch an existing bank account for a user."
    )
    @PostMapping("/getAccount")
    public ResponseEntity<?> getAccountDetails(@RequestBody Account account) {
        Long id = account.getId();
        Optional<Account> accountOpt = accountRepository.findById(id);
        if (accountOpt.isPresent()) {
            return ResponseEntity.ok(accountOpt.get());
        } else {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Account not found");
        }
    }

    @Operation(
            summary = "Deposits the amount",
            description = "This endpoint is used to deposit amount for a user."
    )
    @PostMapping("/deposit")
    public ResponseEntity<DepositAndWithdrawResponse> deposit(@RequestBody DepositAndWithdrawRequest request) {
        Optional<Account> optionalAccount = accountRepository.findById(request.getId());
        if (optionalAccount.isPresent()) {
            Account account = optionalAccount.get();
            account.setBalance(account.getBalance() + request.getAmount());
            accountRepository.save(account);
            DepositAndWithdrawResponse response = new DepositAndWithdrawResponse();
            response.setId(account.getId());
            response.setAccountHolder(account.getAccountHolder());
            response.setBalance(account.getBalance());
            response.setRemarks("Deposit successful. New balance: " + account.getBalance());
            return ResponseEntity.ok(response);
        } else {
            return ResponseEntity.notFound().build();
        }
    }

    @Operation(
            summary = "withdraws the amount",
            description = "This endpoint is used to withdraw amount for a user."
    )
    @PostMapping("/withdraw")
    public ResponseEntity<DepositAndWithdrawResponse> withdraw(@RequestBody DepositAndWithdrawRequest request) {
        Optional<Account> optionalAccount = accountRepository.findById(request.getId());
        if (optionalAccount.isPresent()) {
            Account account = optionalAccount.get();
            if(account.getBalance() >= request.getAmount()){
                account.setBalance(account.getBalance() - request.getAmount());
                accountRepository.save(account);
                DepositAndWithdrawResponse response = new DepositAndWithdrawResponse();
                response.setId(account.getId());
                response.setAccountHolder(account.getAccountHolder());
                response.setBalance(account.getBalance());
                response.setRemarks("Withdraw successful. New balance: " + account.getBalance());
                return ResponseEntity.ok(response);
            }
            else{
                account.setBalance(account.getBalance());
                accountRepository.save(account);
                DepositAndWithdrawResponse response = new DepositAndWithdrawResponse();
                response.setId(account.getId());
                response.setAccountHolder(account.getAccountHolder());
                response.setBalance(account.getBalance());
                response.setRemarks("Withdraw unsuccessful because entered amount was greater than the current balance: " + account.getBalance());
                return ResponseEntity.ok(response);
            }

        } else {
            return ResponseEntity.notFound().build();
        }
    }

    @DeleteMapping("/deleteAccount")
    @Operation(
            summary = "Deletes an existing bank account",
            description = "This endpoint is used to delete an existing bank account for a user."
    )
    public ResponseEntity<String> deleteAccountById(@RequestBody Map<String, Long> request) {
        Long id = request.get("id");
        Optional<Account> account = accountRepository.findById(id);
        if (account.isPresent()) {
            accountRepository.deleteById(id);
            return ResponseEntity.ok("Account with ID " + id + " has been deleted successfully.");
        } else {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Account with ID " + id + " not found.");
        }
    }

}

application.properties:

spring.datasource.url=jdbc:mysql://localhost:3306/bank
spring.datasource.username=root
spring.datasource.password=MyPassword
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.4.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.bank</groupId>
    <artifactId>bankingApplication</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>bankingApplication</name>
    <description>Demo project for Spring Boot</description>
    <url/>
    <licenses>
        <license/>
    </licenses>
    <developers>
        <developer/>
    </developers>
    <scm>
        <connection/>
        <developerConnection/>
        <tag/>
        <url/>
    </scm>
    <properties>
        <java.version>21</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
            <version>2.3.0</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

I added @ControllerAdvice with custom exception classes to return structured error responses. I expected Swagger to continue working as it was earlier, but instead, I got a 500 error at /v3/api-docs. Removing the exception handler fixes Swagger, so I believe something is going wrong inside the exception handling layer. To troubleshoot, I commented out the generalized @ExceptionHandler(Exception.class) and kept only specific ones like MethodArgumentTypeMismatchException and IllegalArgumentException, assuming Swagger might rely on some built-in exception handling. But the result was still the same—Swagger fails to load with a 500 error, while Postman works fine.


Solution

  • You write, that removing the exception handler helps and Swagger starts working again. Perhaps, your @ControllerAdvice catches internal exceptions, possibly during /v3/api-docs generation.

    Try to remove assignableTypes and limit your @RestControllerAdvice with basePackages. Something like:

    @RestControllerAdvice(basePackages = "com.bank.bankingApplication.controller")