Course outline

Spring Boot / Basic: composition and HTTP contracts / Lesson 5

Validation and safe problem details

Separate conversion, Bean Validation, domain invariants, and persistence constraints, then expose stable errors without leaking rejected values or internal causes.

About 5 min readContent reviewed
On this page

Lesson goals

  • Declare and activate the intended validation boundary.
  • Map MVC validation failure to a stable problem representation.
  • Preserve authorization and database checks as separate controls.

Several different boundaries can reject one request

JSON parsing and type conversion happen before a usable request object may exist. Bean Validation checks declared constraints on a constructed value when the integration invokes it. Domain validation enforces business transitions, while database constraints protect stored invariants under concurrent writers. These layers complement one another and should produce distinguishable diagnostic evidence rather than one generic unexpected-error response.

Boot3.5 uses Jakarta validation packages, and a suitable provider/integration is required; add the validation starter deliberately rather than assuming the web starter always supplies every validator. @Valid alone is not a universal command that validates any object anywhere. The call boundary, nested annotations, method signature, and framework configuration determine which validation path actually runs.

Use constraints that express the accepted input

The fragment defines a nonblank name limited to eighty characters and a positive integer price in documented minor units. The controller marks the body @Valid. A missing primitive price can become its default zero before validation, so use a nullable wrapper plus @NotNull if absence must be distinguished from an explicit zero. Choose the representation from the contract rather than convenience.

@Size does not make a whitespace-only name meaningful; @NotBlank supplies a different rule. An email-shaped string does not prove account ownership or uniqueness. Cross-field and state-dependent rules may belong in a domain operation or a custom constraint with a clear failure contract. Keep resource limits on body size and expensive parsing outside assumptions about one field annotation.

java Illustrative Jakarta request validation boundary
package training.catalog;

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.Size;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

record CreateProduct(@NotBlank @Size(max=80) String name,
                     @Positive int unitPriceCents) {}

// Methods shown as an MVC fragment inside a managed controller.
@PostMapping("/products")
ProductView create(@Valid @RequestBody CreateProduct request) {
    return products.create(request);
}

The method fragment needs a managed controller, service, validation provider, and DTO result. It is not a complete runnable Java class.

Publish approved error fields, not raw exceptions

ProblemDetail supports a structured problem response with status, title, detail, and controlled extensions. The advice fragment maps body-validation errors to400 and includes field/code pairs. It intentionally omits rejected values and raw internal exception text. Error order is not promised here; tests should compare the intended set or the application should define a deterministic sort if ordering is part of the contract.

This handler is specific to MethodArgumentNotValidException. MVC method validation can produce HandlerMethodValidationException under applicable signatures, and authentication/authorization failures in filters need their own security handlers. Do not claim one controller advice catches every failure in the system. Domain and infrastructure errors require separate safe mappings, with original causes preserved in restricted diagnostics.

java A specific safe validation-problem mapping
package training.catalog;

import java.util.Map;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
class ValidationAdvice {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    ProblemDetail invalidBody(MethodArgumentNotValidException error) {
        var problem = ProblemDetail.forStatusAndDetail(
            HttpStatus.BAD_REQUEST, "Request validation failed.");
        problem.setTitle("Invalid request");
        problem.setProperty("errors", error.getBindingResult().getFieldErrors().stream()
            .map(field -> Map.of("field", field.getField(),
                "code", field.getCode() == null ? "invalid" : field.getCode()))
            .toList());
        return problem;
    }
}

The intended response omits rejected values and handles one MVC exception family. No HTTP exchange or advice dispatch was executed.

Validation does not authorize or serialize concurrent decisions

A valid product identifier can belong to another tenant. A valid positive quantity can exceed current stock. Two individually valid requests can race against a unique business key. Enforce trusted access scope, guard domain transitions, and keep database constraints and transaction handling in place. Do not translate every DataIntegrityViolationException into a duplicate response without identifying the actual constraint and operation.

Stable public error codes help clients respond without parsing exception prose. Decide which details are safe, how localization works, and whether a request identifier can link the response to restricted logs. Never put passwords, tokens, full personal records, or entire configuration dumps into a validation error merely because they were rejected input.

Test accepted, rejected, and unchanged state

Test malformed JSON, wrong types, missing fields, blank versus empty text, numeric boundaries, nested values, and authorized versus unauthorized requests. A negative test should verify both the public error contract and that no forbidden partial mutation occurred. A direct DTO constructor test cannot establish that MVC actually invokes Bean Validation through the selected signature and dependency set.

Use an appropriately scoped web fixture for binding/advice, domain unit tests for state policy, and database integration tests for concurrent invariants when a controlled database is authorized. The fragments here remain non-runnable and no database is provisioned. Keep successful syntax parsing, intended validation behavior, and observed framework execution labeled separately so evidence remains assessable.

Common errors and fixes

Invalid input reaches the service

Why it happens
Validation was declared but not activated at the actual boundary.
How to fix it
Verify provider dependencies, @Valid placement, and signature behavior.
Prevent it next time
Exercise a scoped MVC rejection test.

A problem response includes secret input

Why it happens
Rejected values or raw exceptions were serialized.
How to fix it
Return approved field/code information and restrict diagnostic content.
Prevent it next time
Test responses with synthetic secret markers.

A401 is not caught by controller advice

Why it happens
The failure occurred in a security filter before MVC invocation.
How to fix it
Configure the appropriate security entry/denial handlers.
Prevent it next time
Test filter and controller boundaries separately.

Check your understanding

1. Why can @Positive int fail to distinguish omitted price from explicit zero?

Show worked solution

A missing primitive value can become zero during binding. Both then violate Positive in the same way. Use a nullable wrapper with NotNull when the product needs separate absence semantics, and define the domain rule rather than relying on an incidental default.

2. A blank name passes a length bound. Which contract is missing?

Show worked solution

A maximum length does not establish meaningful nonblank content. Add the appropriate nonblank rule and decide trimming/normalization explicitly. Test empty and whitespace-only values separately, while remembering that validation still does not prove identity, ownership, or uniqueness.

Key takeaways

  • Parsing, validation, domain policy, and constraints are separate layers.
  • Problem responses expose an approved stable contract.
  • A declared annotation is not proof that the integration executed it.

Official references