Bean Validation
You cannot trust data sent by a client: the title may be empty, the text one letter long, the number negative. Bean Validation lets you describe the rules as annotations right on the fields, and the server performs the check. In Jakarta REST it is enabled automatically: you only need to ask for a method argument to be validated.
Rules on the record
Add constraints to src/main/java/com/example/blog/NewPost.java:
// src/main/java/com/example/blog/NewPost.java
package com.example.blog;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public record NewPost(
@NotBlank(message = "Title is required")
@Size(max = 100, message = "Title must be at most 100 characters")
String title,
@NotBlank(message = "Text is required")
@Size(min = 10, message = "Text must be at least 10 characters")
String body) {
}Without the message attribute the standard message in the server's language is used, for example must not be blank.
Turning validation on
In the resource, mark the argument with @Valid and the path parameter with a @Min constraint:
// src/main/java/com/example/blog/PostResource.java (fragment)
@GET
@Path("/{id}")
public Post show(@PathParam("id") @Min(value = 1, message = "Post id must be greater than zero") long id) {
// ...
}
@POST
@Consumes(MediaType.APPLICATION_JSON)
public Response create(@Valid NewPost data, @Context UriInfo uriInfo) {
// ...
}The imports are jakarta.validation.Valid and jakarta.validation.constraints.Min. When the data breaks the rules, the method is not even called: Jakarta REST answers 400 right away.
curl -i -H "Content-Type: application/json" -d '{"title":"","body":"short"}' http://localhost:8080/blog/api/postsHTTP/1.1 400 Bad Request
Content-Type: text/plain;charset=UTF-8
validation-exception: true
[PARAMETER][create.arg0.title][Title is required][][PARAMETER][create.arg0.body][Text must be at least 10 characters][short]
This response is produced by RESTEasy, the Jakarta REST implementation in Open Liberty. It is awkward for people, so let's make our own.
A JSON response
Validation errors arrive as a ConstraintViolationException, and we already know how to turn exceptions into responses:
// src/main/java/com/example/blog/ValidationMapper.java
package com.example.blog;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.Path;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
import java.util.List;
@Provider
public class ValidationMapper implements ExceptionMapper<ConstraintViolationException> {
public record FieldError(String field, String message) {
}
@Override
public Response toResponse(ConstraintViolationException e) {
List<FieldError> errors = e.getConstraintViolations().stream()
.map(v -> new FieldError(lastName(v), v.getMessage()))
.toList();
return Response.status(Response.Status.BAD_REQUEST)
.type(MediaType.APPLICATION_JSON)
.entity(errors)
.build();
}
private static String lastName(ConstraintViolation<?> violation) {
String name = null;
for (Path.Node node : violation.getPropertyPath()) {
name = node.getName();
}
return name;
}
}The path to a field looks like create.arg0.title; the lastName method takes the last name from it — title. The same request now returns (the order of errors is not guaranteed):
HTTP/1.1 400 Bad Request
Content-Type: application/json
[{"field":"title","message":"Title is required"},{"field":"body","message":"Text must be at least 10 characters"}]
Main constraints
| Annotation | What it checks |
|---|---|
@NotNull | The value is not null |
@NotBlank | The string is not empty and not only spaces |
@Size(min, max) | String length or collection size |
@Min, @Max | Number bounds |
@Email | Looks like an email address |
@Pattern(regexp) | Matches a regular expression |
@Past, @Future | A date in the past or future |
Common mistake
Putting constraints on the fields but forgetting @Valid on the method argument. Then there is no validation at all, and an empty post is saved without complaint:
HTTP/1.1 201 Created
{"body":"short","createdAt":"2026-10-04","id":3,"title":""}
Annotations on fields only describe the rules; @Valid says where to check them.