← Spring Boot®: applications, contracts and operations
04 / 10 · 60 MIN

Validation, errors and response contracts

Distinguish reading, constraints and business rules; keep error responses consistent without hiding conflicts or exposing internal details.

Three different questions

First ask whether the message can be read. Malformed JSON can fail before a DTO exists. Then ask whether values meet constraints such as Min and NotBlank. Finally check context-dependent business rules, such as available capacity, and the user’s permissions.

Two rejections can share 400 while requiring different corrections. A reading error requires fixing the representation; an out-of-range quantity requires fixing the value. A valid DTO proves neither authorization nor current availability. Record the stage and a safe reason instead of reducing everything to HTTP status.

The signature defines the level

Valid requests validation of object constraints; it is not itself a value constraint. With only a Valid DTO and no BindingResult, individual validation can produce MethodArgumentNotValidException. Adding a direct constraint, such as Min on a parameter, requires method validation and can produce HandlerMethodValidationException.

These lessons use built-in MVC support with an available provider and no class-level Validated on the controller. Class-level Validated can involve proxy validation and change the mechanism. During migration, review signature, configuration and handlers together; the annotation name alone does not describe the entire boundary.

Local handling retains responsibility

A BindingResult immediately after the validated argument enables errors to be handled in the method itself. That makes a local response possible, but also permits an implementation mistake: calling the service before checking hasErrors. Reaching the method no longer proves constraint acceptance.

In the illustrative reservation, units=0 and errors.hasErrors()=true should lead to the defined rejection before reserve. Do not remove the constraint to hide the failure. If other constrained parameters have errors without an adjacent Errors argument, method validation can still prevent invocation.

A public error contract

ProblemDetail represents an HTTP problem with standard fields and extensions. When returned directly by a handler, status determines the response status. A stable errorCode can let a client distinguish a capacity conflict without interpreting free text; document the extension and keep it consistent with standard fields.

Choose details that help correct the request without copying stack traces, SQL or secrets. With Jackson, custom properties appear as JSON fields. This format implements no retries and does not determine whether another attempt is safe. The client still needs an explicit policy.

Handler scope and precedence

RestControllerAdvice shares response-body handling across controllers and can restrict applicability through selectors. A matching local handler is considered before global handlers. If one controller returns 200 for the same conflict others represent as 409, inspect its local handling first.

MVC resolution is based on DispatcherServlet. An exception in a filter before invoking it does not automatically enter the same path. Reviewing the boundary where the error originates is more useful than indiscriminately raising advice priority. Security and infrastructure may need their own handling aligned with the public contract.

Three outcomes in one model

Read the example with an available validation provider and JSON converter. units=0 violates Min(1), so the method body is not called. units=3 passes and produces eligible-in-model. units=6 also passes constraints 1..20 but exceeds fictional capacity 4 and throws CapacityConflictException; advice returns 409.

The model neither queries stock nor reserves units. Capacity is a fixed teaching premise, not a production measurement. To standardize built-in MVC errors as ProblemDetail in Boot as well, spring.mvc.problemdetails.enabled can activate that support; the shown advice handles only the custom conflict.

Review the complete contract

A useful review compares readable and unreadable requests, values inside and outside constraints, and a business conflict. For each, identify whether the method was called, whether it could cause effects, the expected status and the public fields. Include a signature with a direct constraint so the second validation exception is not overlooked.

Questions and cases ask you to choose these interpretations from illustrative evidence; they require no Java execution. When accepting a real change, avoid claiming correctness merely because responses contain JSON: format, status meaning, handler scope and effects are separate dimensions.

// Separate files in package com.acme.shipping; inside the application scan root.
// Compatible MVC JSON support and Jakarta Validation provider are prerequisites.
// ReservationPreviewController.java
package com.acme.shipping;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ReservationPreviewController {
  public record Input(@NotBlank String sku, @Min(1) @Max(20) int units) {}
  public record Preview(String sku, int units, String outcome) {}

  @PostMapping(path = "/reservations/preview", consumes = "application/json",
      produces = "application/json")
  public Preview preview(@Valid @RequestBody Input input) {
    if (input.units() > 4) throw new CapacityConflictException();
    return new Preview(input.sku(), input.units(), "eligible-in-model");
  }
}

// CapacityConflictException.java
package com.acme.shipping;
public class CapacityConflictException extends RuntimeException {}

// ReservationErrors.java
package com.acme.shipping;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice(assignableTypes = ReservationPreviewController.class)
public class ReservationErrors {
  @ExceptionHandler(CapacityConflictException.class)
  public ProblemDetail capacityConflict() {
    ProblemDetail problem = ProblemDetail.forStatusAndDetail(
        HttpStatus.CONFLICT, "Requested units exceed this model's capacity.");
    problem.setTitle("Capacity conflict");
    problem.setProperty("errorCode", "CAPACITY_CONFLICT");
    return problem;
  }
}
IN PRACTICE

With sku="BOX-A", compare units=0, 3 and 6. Zero violates Min before the method; three returns a DTO; six passes constraints but meets illustrative capacity 4 and produces a conflict. No branch writes or reserves stock.

Common pitfalls

Using Valid as authorization; ignoring BindingResult; handling only one validation exception; returning 200 for every error; copying internal messages to clients; expecting advice to catch every filter failure.

Related topics: HTTP routing and request contracts · Security and authorization boundaries · Test scope and integration evidence

Take this idea with you

Rejection should occur at the right boundary with consistent status and body. A structured response informs the client but does not automatically correct the request or guarantee safe retries.

Create account

Reference: MVC validation boundaries · Spring Boot 4.1.1 / Spring Framework 7.0.9 / Java 25