DEV Community

Devanshu Biswas
Devanshu Biswas

Posted on • Originally published at github.com

I built a Bean Validation playground: watch @Valid constraints fail and Spring's 400 body build itself, live

Every Spring dev has shipped a @NotNull and then been confused when a nested object wasn't validated, or when the 400 response didn't look how they expected. Bean Validation is one of those things that's obvious in theory and full of small gotchas in practice. So I built a playground where you edit a request DTO and watch every constraint — and the exact error JSON — update live.

▶ Live demo: https://dev48v.github.io/bean-validation/
Source: https://github.com/dev48v/bean-validation

The DTO you're validating

public class CreateUserRequest {
  @NotBlank @Size(min = 3, max = 20)
  private String username;

  @NotBlank @Email
  private String email;

  @NotNull @Min(18) @Max(120)
  private Integer age;

  @NotBlank @Size(min = 8, max = 64)
  @Pattern(regexp = "(?=.*\\d)(?=.*[A-Z]).+")
  private String password;

  @Valid                 // ← cascades into Address
  private Address address;
}

@PostMapping("/api/users")
ResponseEntity<?> create(@Valid @RequestBody CreateUserRequest req) {  }
Enter fullscreen mode Exit fullscreen mode

In the demo, each annotation on each field flips green or red as you type, using the real Hibernate Validator default messages.

Three things it makes obvious

1. Every constraint reports independently. password has @NotBlank @Size @Pattern all at once. Type weak and you get two separate violations — one for size, one for the pattern. The validator doesn't stop at the first failure; it collects them all, which is why your errors[] array can have multiple entries for one field.

2. @Valid is what makes nested validation run. The address object is only checked because the field is annotated @Valid. This is the #1 Bean Validation bug I see: people put constraints on Address.street, wire up the endpoint, and can't figure out why an empty street sails through. The constraints on the nested class do nothing unless the reference to it carries @Valid. Remove it and Spring skips the whole object.

3. The 400 body has a specific shape. When validation fails on a @RequestBody, Spring throws MethodArgumentNotValidException, and (with a typical handler) you get:

{
  "status": 400,
  "error": "Bad Request",
  "message": "Validation failed for object='createUserRequest'. Error count: 7",
  "errors": [
    { "field": "email", "rejectedValue": "not-an-email", "message": "must be a well-formed email address" },
    { "field": "address.zip", "rejectedValue": "ABC", "message": "must be a 5-digit ZIP" }
  ],
  "path": "/api/users"
}
Enter fullscreen mode Exit fullscreen mode

Note address.zip — nested field errors come back with the dotted path, which is how you map them onto a form. The demo builds this body live, so you can watch entries appear and disappear as you fix fields.

@Valid vs @Validated — the other gotcha

Two things people mix up:

  • @Valid (Jakarta) on a @RequestBody parameter or a nested field — triggers validation and cascades.
  • @Validated (Spring) at the class level — enables method-level validation and validation groups (e.g. validate different constraints on create vs update).

For request-body DTOs you almost always want @Valid on the parameter. Reach for @Validated when you need groups or you're validating method arguments on a @Service.

Hit break it in the demo to fail every constraint at once, then fix them one by one and watch the error list shrink. Once the cascade rule and the response shape are muscle memory, Bean Validation stops surprising you.

If it helped, a star helps others find it: https://github.com/dev48v/bean-validation

Top comments (0)