Spring Boot Validation — 2GB Payload Crashes Without @Valid
Learn why omitting @Valid in Spring Boot can silently crash your app with large payloads.
20+ years shipping production Java in banking & fintech. Everything here is grounded in real deployments.
- ✓Java 17+ (we use records and text blocks)
- ✓Spring Boot 3.2+ (or 2.7+ with javax.validation)
- ✓Maven or Gradle build tool
- ✓Basic understanding of REST controllers and DTOs
• @Valid triggers Bean Validation (JSR-380) on request bodies, rejecting invalid data early • Without @Valid, Spring Boot accepts any payload, even malformed 2GB JSON, causing OOM or silent data corruption • Always use @Valid on @RequestBody in controllers; pair with @Validated for group validation • For large payloads, add @Size on collections/strings and configure max request size in application.properties • Use global exception handling with @ControllerAdvice to return consistent 400 responses on validation failure
Think of @Valid like a bouncer at a nightclub. Without a bouncer, anyone can walk in—even a drunk guy carrying a 2-ton sofa (your 2GB payload). The sofa crushes the floor (your JVM heap) and everyone inside (your app) dies. With @Valid, the bouncer checks IDs (field constraints) and turns away anything suspicious before it enters the party.
I've been debugging Spring Boot apps since 1.5.2.RELEASE, and I've seen production meltdowns that trace back to a single missing annotation: @Valid. The most memorable was a SaaS billing service that accepted a 2.1GB JSON payload from a misconfigured client. No @Valid on the controller. No @Size on the List field. The app tried to deserialize the entire monstrosity into memory, hit the heap limit, and OOM-killed the pod. The billing pipeline went down for 45 minutes. The root cause? A junior dev forgot @Valid on a single @RequestBody parameter. That's it. One annotation. 45 minutes of downtime. In this tutorial, I'll show you exactly how @Valid works under the hood, why skipping it is dangerous, and how to build bulletproof validation for REST APIs that handle anything from tiny forms to massive payloads. We'll cover JSR-380 Bean Validation, custom validators, group sequences, and production patterns like logging validation failures without exposing internals. By the end, you'll never ship a controller without @Valid again.
Setting Up the Project
Let's start with a Spring Boot 3.2 project using Maven. We'll add the validation starter which pulls in Hibernate Validator 8.0 (the reference implementation of JSR-380). Create a new project with spring-boot-starter-web and spring-boot-starter-validation. In your pom.xml, include:
This starter automatically configures a LocalValidatorFactoryBean and registers it with Spring MVC. No extra configuration needed. For Java 17+, we'll use records as DTOs — they're immutable and perfect for validation. Here's a simple Invoice record with constraints: @NotNull on the id, @NotBlank on the customerEmail, @Positive on the amount, and @Size(max = 10) on lineItems to prevent huge lists. We'll also add a custom error message for each constraint. The key is that without @Valid on the controller parameter, none of these constraints fire. Spring just deserializes the JSON and passes the object straight to your method.
What the Official Docs Won't Tell You
The Spring Boot reference docs show you how to use @Valid on a single @RequestBody parameter. They don't tell you that if you forget it, your app will silently accept any payload — including a 2GB JSON array that causes OOM. They don't tell you that @Valid only triggers cascading validation on nested objects if you also annotate the nested fields with @Valid. They don't tell you that validation errors by default return a 400 with a stack trace in the response body, leaking internal class names. They don't tell you that @Valid and @Validated are not interchangeable: @Valid is JSR-380 standard, @Validated is Spring's extension that enables validation groups. Use @Validated on the class level for method-level validation (like @PathVariable), and @Valid on parameters. Also, the docs gloss over the fact that validation runs AFTER Jackson deserialization. If you have a @Size(max = 100) on a List, Jackson still allocates the entire array before validation rejects it. To prevent OOM, you need to set spring.jackson.deserialization.max-array-length in application.properties. That's a Jackson-level safeguard that runs before validation.
Creating a Validated DTO with Records
Java 16+ records are perfect for DTOs — they're immutable, concise, and support Bean Validation annotations out of the box. Here's an Invoice record with constraints on every field. Notice the @Valid annotation on the List
Building a Controller with @Valid
Here's the controller that caused our production incident. The method createInvoice accepts an @RequestBody Invoice without @Valid. Spring Boot will deserialize the JSON into an Invoice record, but NONE of the validation constraints on Invoice or LineItem will fire. The method receives whatever Jackson produces — even if fields are null or the list has 2 million items. To fix this, add @Valid before the @RequestBody parameter. Now Spring MVC will call the Validator before executing the method. If validation fails, a MethodArgumentNotValidException is thrown, which we can handle globally. Notice we also use @Validated on the class level — this enables method-level validation for other parameters like @PathVariable or @RequestParam. But for @RequestBody, @Valid is the standard. Also note the use of BindingResult parameter: if you add BindingResult immediately after @Valid, you can handle validation errors manually. But in production, use global exception handling instead — it's cleaner and consistent.
Global Exception Handling for Validation Errors
Without a global exception handler, a validation failure returns a 400 with a stack trace and internal class names like 'jakarta.validation.ConstraintViolationException'. That's a security risk. Use @ControllerAdvice to catch MethodArgumentNotValidException (for @Valid on parameters) and ConstraintViolationException (for @Validated on class-level). Return a consistent JSON error response with field-level messages. Here's a handler that extracts field errors and returns a map of field names to error messages. We also log the validation failure at WARN level for debugging, but never include the full stack trace in the response. For production, you might want to include a correlation ID so you can trace the request in logs. Also handle HttpMessageNotReadableException for malformed JSON — this catches cases where Jackson fails to deserialize. The key is to never expose internal implementation details in error responses.
Custom Validators for Business Rules
Built-in constraints like @NotNull and @Size cover basic cases, but real-world apps need business rule validation. For example, an invoice's total amount must equal the sum of its line item amounts. Create a custom annotation @InvoiceTotalValid and a validator class that implements ConstraintValidator. The validator receives the Invoice record and can access all fields. If the total doesn't match, return false and a message. Register the annotation on the Invoice record itself. This is class-level validation, which runs after all field-level validations pass. Also note that custom validators can inject Spring beans (like a repository) if you implement ApplicationContextAware or use @Autowired in the validator. But be careful: validators are singletons, so inject only stateless beans. For stateful operations, use a separate service. Custom validators are powerful but can be slow — avoid database calls in validators if possible.
Validation Groups for Partial Updates
In REST APIs, POST (create) and PUT (update) often have different validation rules. For example, during create, the id field must be null (auto-generated). During update, id must be present. Use validation groups to handle this. Define marker interfaces: OnCreate and OnUpdate. Annotate fields with @NotNull(groups = OnUpdate.class) and @Null(groups = OnCreate.class). In the controller, use @Validated(OnCreate.class) on the parameter for POST and @Validated(OnUpdate.class) for PUT. Note: @Validated is Spring's annotation for group-based validation — you cannot use @Valid with groups. Also, @Validated on the controller method replaces @Valid. This is a common point of confusion. Groups are powerful but add complexity. Only use them when you have genuinely different validation rules between operations. For simple cases, use separate DTOs for create and update.
Testing Validation with JUnit 5 and MockMvc
Unit tests for validation are non-negotiable. Use MockMvc to send invalid payloads and assert the response status and error messages. Here's a test that sends an invoice with a blank email and a negative amount. The test expects HTTP 400 and specific error messages. Use @WebMvcTest to load only the controller layer. Mock the service layer if needed. For the test to work, the ValidationAutoConfiguration must be active — it is by default with @WebMvcTest. Also test edge cases: null body, empty list, oversized list. Don't forget to test the happy path too — a valid payload should return 200. For integration tests, use @SpringBootTest with a random port and TestRestTemplate. This catches configuration issues like missing validation starter. In CI, run validation tests as part of every build. We once pushed a change that accidentally removed @Valid from a controller — the tests caught it immediately.
The 2GB Payload That Killed Our Billing Service
- Always annotate @RequestBody with @Valid in controllers.
- Add @Size constraints on collections and strings to limit payload size.
- Configure global max request size at the servlet container level.
- Monitor heap usage and set alerts for OOM conditions.
grep -r '@RequestBody' src/main/java/ | grep -v '@Valid'curl -v -X POST -H 'Content-Type: application/json' -d '{"id":null}' http://localhost:8080/api/invoices| File | Command / Code | Purpose |
|---|---|---|
| pom.xml | Setting Up the Project | |
| application.properties | spring.jackson.deserialization.max-array-length=10000 | What the Official Docs Won't Tell You |
| Invoice.java | public record Invoice( | Creating a Validated DTO with Records |
| InvoiceController.java | @Validated | Building a Controller with @Valid |
| GlobalExceptionHandler.java | @ControllerAdvice | Global Exception Handling for Validation Errors |
| InvoiceTotalValidator.java | public class InvoiceTotalValidator | Custom Validators for Business Rules |
| InvoiceWithGroups.java | public record InvoiceWithGroups( | Validation Groups for Partial Updates |
| InvoiceControllerTest.java | @WebMvcTest(InvoiceController.class) | Testing Validation with JUnit 5 and MockMvc |
Key takeaways
Interview Questions on This Topic
Explain how @Valid triggers validation in Spring MVC. What happens if you omit it?
Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Everything here is grounded in real deployments.
That's Spring Boot. Mark it forged?
5 min read · try the examples if you haven't