Bean Validation
Нельзя доверять данным, которые прислал клиент: заголовок может быть пустым, текст — из одной буквы, номер — отрицательным. Bean Validation позволяет описать правила аннотациями прямо на полях, а проверку выполнит сервер. В Jakarta REST она подключена автоматически: достаточно попросить проверить аргумент метода.
Правила на записи
Добавьте ограничения в 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 = "Заголовок обязателен")
@Size(max = 100, message = "Заголовок не длиннее 100 символов")
String title,
@NotBlank(message = "Текст обязателен")
@Size(min = 10, message = "Текст не короче 10 символов")
String body) {
}Без атрибута message используется стандартное сообщение на языке сервера, например must not be blank.
Включаем проверку
В ресурсе пометьте аргумент аннотацией @Valid, а параметр пути — ограничением @Min:
// src/main/java/com/example/blog/PostResource.java (фрагмент)
@GET
@Path("/{id}")
public Post show(@PathParam("id") @Min(value = 1, message = "Номер записи должен быть больше нуля") long id) {
// ...
}
@POST
@Consumes(MediaType.APPLICATION_JSON)
public Response create(@Valid NewPost data, @Context UriInfo uriInfo) {
// ...
}Импорты — jakarta.validation.Valid и jakarta.validation.constraints.Min. Если данные нарушают правила, метод даже не вызывается: Jakarta REST сразу отвечает 400.
curl -i -H "Content-Type: application/json" -d '{"title":"","body":"коротко"}' 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][Заголовок обязателен][][PARAMETER][create.arg0.body][Текст не короче 10 символов][коротко]
Такой ответ формирует RESTEasy — реализация Jakarta REST в Open Liberty. Для людей он неудобен, поэтому сделаем свой.
Ответ в JSON
Ошибки валидации приходят как исключение ConstraintViolationException, а исключения мы уже умеем превращать в ответы:
// 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;
}
}Путь к полю выглядит как create.arg0.title; метод lastName берёт из него последнее имя — title. Тот же запрос теперь возвращает (порядок ошибок не гарантирован):
HTTP/1.1 400 Bad Request
Content-Type: application/json
[{"field":"title","message":"Заголовок обязателен"},{"field":"body","message":"Текст не короче 10 символов"}]
Основные ограничения
| Аннотация | Что проверяет |
|---|---|
@NotNull | Значение не null |
@NotBlank | Строка не пустая и не из одних пробелов |
@Size(min, max) | Длина строки или размер коллекции |
@Min, @Max | Границы числа |
@Email | Похоже на адрес почты |
@Pattern(regexp) | Совпадение с регулярным выражением |
@Past, @Future | Дата в прошлом или будущем |
Частая ошибка
Поставить ограничения на поля, но забыть @Valid у аргумента метода. Тогда проверки не будет вообще, и пустая запись спокойно сохранится:
HTTP/1.1 201 Created
{"body":"коротко","createdAt":"2026-10-04","id":3,"title":""}
Аннотации на полях только описывают правила, а @Valid говорит, где их проверять.