Skip to content
AITroveRead. Build. Understand.
Make this comfortable

Spring MVC request validation: reject invalid commands before mutation

Last updated: 30 Sept 20264 min read
tutorial
IntermediateBy AITrove Editorial

Spring MVC can deserialize a request body and validate its declared constraints before a controller passes the command to a service.

Download Spring source kit

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

Java
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.

spring
spring-boot
rest-validation
Storage details