Spring MVC can deserialize a request body and validate its declared constraints before a controller passes the command to a service.
Spring MVC request validation: reject invalid commands before mutation
This lesson uses the downloadable source kit: Java 21, Spring Boot 4.0.8 and its managed Spring Framework 7 dependencies. The version is pinned for repeatable builds.
Keep the input command small
CreateReceipt contains only amountMinor. Positive validation rejects zero before create() mutates the store; the store repeats its domain check so a non-HTTP caller cannot bypass the rule. Transport validation and business invariants serve different callers.
The successful request returns 201 with the created receipt. The zero-amount request returns a documented 400 response through ReceiptErrors. Both tests use JSON input, so they check conversion and validation rather than merely calling the Java method.
Validation is not authorization
A positive amount does not establish who may create a receipt or which tenant owns it. Request authorization and tenant ownership require their own rules. Never mass-bind persistence fields such as owner, approved or internal state directly from a public command.
The controller fixture returns an in-memory receipt. It does not implement persistence, payment processing or idempotent retries. The integrated project documents that limit instead of treating a successful JSON response as proof of durable storage.
Checked source
package in.aitrove.learning;
import java.util.List;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Positive;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/receipts")
public class ReceiptController {
public record CreateReceipt(@Positive int amountMinor) {}
private final ReceiptStore store;
public ReceiptController(ReceiptStore store) { this.store = store; }
@PostMapping @ResponseStatus(HttpStatus.CREATED)
public ReceiptStore.Receipt create(@Valid @RequestBody CreateReceipt command) { return store.create(command.amountMinor()); }
@GetMapping("/{id}") public ReceiptStore.Receipt find(@PathVariable long id) {
return store.find(id).orElseThrow(() -> new MissingReceipt(id));
}
@GetMapping public List<ReceiptStore.Receipt> page(@RequestParam(defaultValue="0") int offset,
@RequestParam(defaultValue="20") int limit) { return store.page(offset, limit); }
public static class MissingReceipt extends RuntimeException {
MissingReceipt(long id) { super("Receipt " + id + " was not found"); }
}
}Test the boundary
Run mvn test in the source-kit directory. ReceiptBoundaryTest.createsValidReceipt and rejectsZeroAmount checks the behavior described here. Java excerpts belong to the named source-kit classes; they are not independent source files unless the complete class is shown.
Costs and boundaries
Validation work grows with the command’s fields and constraints. Body-size limits belong at the HTTP/container boundary as well; rejecting a value after deserializing a huge payload does not recover the memory already used.
Common Mistakes
- Do not trust transport validation as the only domain guard.
- Do not bind ownership fields directly from untrusted JSON.
- A valid command still needs authorization.
Read next
Problem details, Security filter chain, Java records: value carriers and defensive copies.
Extend this boundary
Continue with Java UTF-8 decoding: reject malformed bytes before parsing, Java strict date parsing: reject impossible calendar input.
Continue with the new boundary checks
Continue with Spring MVC conditional GET: validate the representation tag.
Continue with collection and web contracts
Continue with Spring MVC consumes and produces: 415 and 406 are different failures, Spring MVC nested validation: reject a bad line before the handler.
Continue with checked Spring boundaries
Continue with Spring MVC validation paths: object constraints and method parameters.
Continue with checked upload and readiness
Continue with Spring MVC multipart receipt import: validate before storing.
