Skip to content

Adopt 422 Unprocessable Entity for payload validation errors #319

Description

@nanotaboada

Problem

Field validation failures currently return 400 Bad Request, which conflates two distinct error categories:

  • 400 Bad Request — the request is malformed (unparseable JSON, wrong Content-Type)
  • 422 Unprocessable Entity — the request is syntactically valid but fails business/field validation rules

Proposed Solution

Return 422 Unprocessable Entity for field validation failures and reserve 400 Bad Request for genuinely malformed requests.

Replace HttpStatus.BAD_REQUEST with HttpStatus.UNPROCESSABLE_ENTITY in controller branches that handle validation failures (e.g. @Valid constraint violations, squad number mismatch). Spring's default HttpMessageNotReadableException handler (unparseable JSON) should remain 400.

Acceptance Criteria

  • Field validation failures return 422 Unprocessable Entity
  • 400 Bad Request is reserved for malformed requests (unparseable JSON, wrong Content-Type)
  • OpenAPI @ApiResponse annotations updated to declare 422 instead of 400 for validation responses
  • Tests updated to assert the new status code
  • All existing tests continue to pass
  • CHANGELOG.md updated

References

Activity

  1. added
    enhancementNew feature or request
    javaPull requests that update Java code
    planningEnables automatic issue planning with CodeRabbit
    priority:lowNice-to-have improvement. Can be deferred without blocking other work.
    and removed
    planningEnables automatic issue planning with CodeRabbit
    on Apr 12, 2026
  2. nanotaboada commented on Apr 14, 2026

    @nanotaboada
    OwnerAuthor
  3. coderabbitai commented on Apr 14, 2026

    @coderabbitai

    Coding Plan

    Summary

    • Introduce a minimal @ControllerAdvice that only handles MethodArgumentNotValidException, returning 422 while letting Spring's default handlers preserve 400 for malformed requests
    • Update the single explicit HttpStatus.BAD_REQUEST in the controller to UNPROCESSABLE_ENTITY for consistency
    • Align OpenAPI annotations and tests with the new status code without changing response body structure or adding new abstractions

    Implementation Steps

    Phase 1: Create Global Exception Handler for Validation Errors

    Introduce a @ControllerAdvice to intercept MethodArgumentNotValidException (thrown when @Valid fails) and return 422 instead of Spring's default 400. This cleanly separates validation failures from malformed request errors.

    Create GlobalExceptionHandler Class

    Create a centralized exception handler to customize the response for validation failures while preserving Spring's default handling for other error types.

    • Create GlobalExceptionHandler.java in the controllers package (following the existing convention of keeping HTTP-layer concerns together)
    • Annotate the class with @ControllerAdvice and @RestController
    • Add an @ExceptionHandler method for MethodArgumentNotValidException that returns ResponseEntity with HttpStatus.UNPROCESSABLE_ENTITY (422)
    • Keep the response body empty (.build()) to match the existing controller pattern—no structured error response is required by the ticket
    • Do NOT add handlers for HttpMessageNotReadableException or other exceptions—let Spring's defaults handle those to preserve 400 for malformed JSON
    🤖 Prompt for AI agents
    Create a new `GlobalExceptionHandler.java` file in the `controllers` package to
    handle validation exceptions globally and return HTTP 422 instead of Spring's
    default 400.
    
    - Annotate the class with both `@ControllerAdvice` and `@RestController`
    - Add a single `@ExceptionHandler` method targeting
    `MethodArgumentNotValidException`
    - The method should return a `ResponseEntity` built with
    `HttpStatus.UNPROCESSABLE_ENTITY` (422) and an empty body (`.build()`)
    - Do NOT add handlers for any other exception types (e.g.,
    `HttpMessageNotReadableException`); leave all other error handling to Spring's
    defaults so that 400 is preserved for malformed JSON and other request errors
    

    Phase 2: Update Controller and OpenAPI Annotations

    Modify the controller to return 422 for the explicit squad number mismatch validation and update the OpenAPI documentation to reflect the new status code for all validation errors.

    Task 1: Update Explicit Status Code in PUT Endpoint

    Change the squad number mismatch check from 400 to 422 since this is a business validation failure, not a malformed request.

    • In src/main/java/.../controllers/PlayersController.java, locate the PUT endpoint's squad number mismatch check (around line 214)
    • Replace HttpStatus.BAD_REQUEST with HttpStatus.UNPROCESSABLE_ENTITY
    Task 2: Update `@ApiResponse` Annotations

    Update the OpenAPI documentation to declare 422 for validation failures while keeping 400 reserved for malformed requests.

    • In PlayersController.java, update the POST endpoint's @ApiResponse annotation: change responseCode = "400" to responseCode = "422" and update description to "Unprocessable Entity - Validation failure"
    • Update the PUT endpoint's @ApiResponse annotation similarly: change 400 to 422 with description "Unprocessable Entity - Validation failure"
    • Keep the existing annotation structure and @Content patterns consistent with other responses
    🤖 Prompt for AI agents
    Update `PlayersController.java` to replace all validation-related HTTP 400
    usages with HTTP 422, including both runtime logic and OpenAPI annotations.
    
    **Task 1 – Fix explicit status code in the PUT endpoint:**
    - Locate the squad number mismatch validation check in the PUT endpoint (around
    line 214)
    - Replace `HttpStatus.BAD_REQUEST` with `HttpStatus.UNPROCESSABLE_ENTITY`
    
    **Task 2 – Update `@ApiResponse` annotations:**
    - On the POST endpoint, find the `@ApiResponse` with `responseCode = "400"` and
    update it to `responseCode = "422"` with description `"Unprocessable Entity -
    Validation failure"`
    - Apply the same change to the PUT endpoint's corresponding `@ApiResponse`
    annotation
    - Preserve all existing `@Content` patterns and the overall annotation
    structure; only the response code value and description string should change
    

    Phase 3: Update Tests and Documentation

    Align tests with the new 422 status code and document the change in the changelog.

    Task 1: Update Test Assertions

    Modify all test methods that assert 400 for validation failures to assert 422 instead.

    • In src/test/java/.../controllers/PlayersControllerTests.java, update four test methods:
      • givenInvalidPlayer_whenPost_thenReturnsBadRequest → change assertion to HttpStatus.UNPROCESSABLE_ENTITY.value() and consider renaming to ...thenReturnsUnprocessableEntity
      • givenInvalidPlayer_whenPut_thenReturnsBadRequest → same changes
      • givenSquadNumberMismatch_whenPut_thenReturnsBadRequest → same changes
      • givenNullBodySquadNumber_whenPut_thenReturnsBadRequest → same changes
    • Rename test methods to reflect the new expected status (replace BadRequest with UnprocessableEntity in method names)
    Task 2: Update CHANGELOG.md

    Document the semantic change following the existing changelog format.

    • Add entry to the [Unreleased] section under ### Changed
    • Follow the established bullet point format: describe that validation failures now return 422 Unprocessable Entity instead of 400 Bad Request
    • Reference the ticket/issue number if applicable
    🤖 Prompt for AI agents
    Update the test file and changelog to reflect the new HTTP 422 status code for
    validation failures.
    
    **Task 1 – Update `PlayersControllerTests.java`:**
    - Locate the following four test methods and apply changes to each:
      - `givenInvalidPlayer_whenPost_thenReturnsBadRequest`
      - `givenInvalidPlayer_whenPut_thenReturnsBadRequest`
      - `givenSquadNumberMismatch_whenPut_thenReturnsBadRequest`
      - `givenNullBodySquadNumber_whenPut_thenReturnsBadRequest`
    - In each method, change the status assertion from
    `HttpStatus.BAD_REQUEST.value()` to `HttpStatus.UNPROCESSABLE_ENTITY.value()`
    - Rename each method by replacing `BadRequest` with `UnprocessableEntity` in the
    method name
    
    **Task 2 – Update `CHANGELOG.md`:**
    - In the `[Unreleased]` section, add or append a `### Changed` subsection
    - Add a bullet point noting that validation failures now return 422
    Unprocessable Entity instead of 400 Bad Request
    - Follow the existing bullet point format; include a ticket/issue reference if
    one is available
    
    Research

    This is a Spring Boot REST API for managing football players. The single PlayersController handles CRUD operations with @Valid annotations on POST and PUT endpoints that trigger Bean Validation (JSR-380) on PlayerDTO. Currently, there is no @ControllerAdvice—Spring Boot's default exception handling returns 400 for validation failures. One explicit HttpStatus.BAD_REQUEST exists in the PUT endpoint for squad number mismatch validation. Tests use MockMvc with AssertJ BDD-style assertions, with four tests currently expecting 400 for validation failures.


    🚀 Next Steps

    🤖 All AI agent prompts combined
    Task: 1
    
    Create a new `GlobalExceptionHandler.java` file in the `controllers` package to
    handle validation exceptions globally and return HTTP 422 instead of Spring's
    default 400.
    
    - Annotate the class with both `@ControllerAdvice` and `@RestController`
    - Add a single `@ExceptionHandler` method targeting
    `MethodArgumentNotValidException`
    - The method should return a `ResponseEntity` built with
    `HttpStatus.UNPROCESSABLE_ENTITY` (422) and an empty body (`.build()`)
    - Do NOT add handlers for any other exception types (e.g.,
    `HttpMessageNotReadableException`); leave all other error handling to Spring's
    defaults so that 400 is preserved for malformed JSON and other request errors
    ===============================================================================
    
    Task: 2
    
    Update `PlayersController.java` to replace all validation-related HTTP 400
    usages with HTTP 422, including both runtime logic and OpenAPI annotations.
    
    **Task 1 – Fix explicit status code in the PUT endpoint:**
    - Locate the squad number mismatch validation check in the PUT endpoint (around
    line 214)
    - Replace `HttpStatus.BAD_REQUEST` with `HttpStatus.UNPROCESSABLE_ENTITY`
    
    **Task 2 – Update `@ApiResponse` annotations:**
    - On the POST endpoint, find the `@ApiResponse` with `responseCode = "400"` and
    update it to `responseCode = "422"` with description `"Unprocessable Entity -
    Validation failure"`
    - Apply the same change to the PUT endpoint's corresponding `@ApiResponse`
    annotation
    - Preserve all existing `@Content` patterns and the overall annotation
    structure; only the response code value and description string should change
    ===============================================================================
    
    Task: 3
    
    Update the test file and changelog to reflect the new HTTP 422 status code for
    validation failures.
    
    **Task 1 – Update `PlayersControllerTests.java`:**
    - Locate the following four test methods and apply changes to each:
      - `givenInvalidPlayer_whenPost_thenReturnsBadRequest`
      - `givenInvalidPlayer_whenPut_thenReturnsBadRequest`
      - `givenSquadNumberMismatch_whenPut_thenReturnsBadRequest`
      - `givenNullBodySquadNumber_whenPut_thenReturnsBadRequest`
    - In each method, change the status assertion from
    `HttpStatus.BAD_REQUEST.value()` to `HttpStatus.UNPROCESSABLE_ENTITY.value()`
    - Rename each method by replacing `BadRequest` with `UnprocessableEntity` in the
    method name
    
    **Task 2 – Update `CHANGELOG.md`:**
    - In the `[Unreleased]` section, add or append a `### Changed` subsection
    - Add a bullet point noting that validation failures now return 422
    Unprocessable Entity instead of 400 Bad Request
    - Follow the existing bullet point format; include a ticket/issue reference if
    one is available
    
    💡 Iterate on the plan with: @coderabbitai <feedback>
    Example Feedback
    - `@coderabbitai` You can skip phase 3. Add a simple unit test case for phase 2.
    - `@coderabbitai` For design choice 1 go ahead with option 3 and replan.
    

    💬 Have feedback or questions? Drop into our discord!

  4. pinned this issue on Apr 19, 2026
  5. nanotaboada commented on Apr 19, 2026

    @nanotaboada
    OwnerAuthor

    Implemented in #323.

  6. added 2 commits that reference this issue on Apr 19, 2026
  7. unpinned this issue on Apr 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestjavaPull requests that update Java codeplanningEnables automatic issue planning with CodeRabbitpriority:lowNice-to-have improvement. Can be deferred without blocking other work.

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions