When a client sends data that fails basic formatting rules (e.g., a blank name or a negative transaction amount), Spring Boot automatically intercepts the request before it reaches your controller and throws a standard

Important

MethodArgumentNotValidException

Because this exception= comes directly= from the Spring Framework, it doesn't extend our custom BusinessException. However, we can handle it seamlessly within the same GlobalExceptionHandler to keep the API responses unified.

Step 1: Add Validation Constraints to Your Request DTO

Use standard Jakarta Bean Validation annotations (like @NotBlank, @Positive) on your incoming data transfer objects (DTOs).

public class AccountCreationRequest {
 
    @NotBlank(message = "Account holder name cannot be blank")
    private String holderName;
 
    @Positive(message = "Initial deposit amount must be greater than zero")
    private double initialDeposit;
 
    // Getters and Setters
}

Step 2: Activate Validation in Your Controller

Place the @Valid annotation right before your request body in the controller method. This tells Spring to validate the incoming JSON before executing any code.

@RestController
@RequestMapping("/api/accounts")
public class AccountController {
 
    @PostMapping
    public ResponseEntity<AccountResponse> createAccount(@Valid @RequestBody AccountCreationRequest request) {
        // If validation fails, this block is never reached.
        return ResponseEntity.ok(accountService.create(request));
    }
}

Step 3: Map Validation Failures in the Global Handler

Add a dedicated @ExceptionHandler method inside your existing GlobalExceptionHandler class. Since a request might contain multiple invalid fields, we should extract all of them into a clean, scannable map.

@ControllerAdvice
public class GlobalExceptionHandler {
 
    // 1. Handles all custom domain errors (AccountNotFoundException, etc.)
    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ErrorResponse> handleBusinessExceptions(BusinessException ex) {
        ErrorResponse error = new ErrorResponse(
            ex.getStatusCode(),
            ex.getErrorCode(),
            ex.getMessage(),
            LocalDateTime.now()
        );
        return ResponseEntity.status(ex.getStatusCode()).body(error);
    }
 
    // 2. NEW: Handles all Spring payload validation errors (HTTP 400 Bad Request)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ValidationErrorResponse> handleValidationExceptions(MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        
        // Loop through all field errors and extract the message
        ex.getBindingResult().getFieldErrors().forEach(error -> 
            errors.put(error.getField(), error.getDefaultMessage())
        );
 
        ValidationErrorResponse validationError = new ValidationErrorResponse(
            HttpStatus.BAD_REQUEST.value(),
            "ERR_VALIDATION_FAILED",
            "Provided request payload contains invalid fields",
            errors,
            LocalDateTime.now()
        );
 
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(validationError);
    }
}

JSON Response Payload

If a frontend client attempts to create an account with a blank name and a negative deposit (-500.00), they will receive a clean 400 Bad Request status containing this precise, structured validation summary:

{
  "status": 400,
  "errorCode": "ERR_VALIDATION_FAILED",
  "message": "Provided request payload contains invalid fields",
  "fieldErrors": {
    "holderName": "Account holder name cannot be blank",
    "initialDeposit": "Initial deposit amount must be greater than zero"
  },
  "timestamp": "2026-09-23T00:35:00"
}
  • Service layers handle complex business flow rules using Optional and clean BusinessException constructs.
  • Controllers stay entirely hands-off regarding validation logic.
  • Clients always receive predictable, machine-readable JSON error contracts regardless of what went wrong inside the application.