Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

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/posts
HTTP/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

AnnotationWhat it checks
@NotNullThe value is not null
@NotBlankThe string is not empty and not only spaces
@Size(min, max)String length or collection size
@Min, @MaxNumber bounds
@EmailLooks like an email address
@Pattern(regexp)Matches a regular expression
@Past, @FutureA 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.

Thanks for reading!

Donate to the site — we work hard for you.