JSON и JSON-B
API обычно обменивается данными в формате JSON. Писать его вручную не нужно: в Jakarta EE есть спецификация JSON-B (JSON Binding), которая превращает Java-объекты в JSON и обратно. Jakarta REST вызывает её сам, если метод помечен @Produces(MediaType.APPLICATION_JSON). Поэтому методы ресурса просто возвращают объекты.
Модель данных
Запись блога удобно описать как record. JSON-B поддерживает записи начиная с Jakarta EE 10.
// src/main/java/com/example/blog/Post.java
package com.example.blog;
import java.time.LocalDate;
public record Post(long id, String title, String body, LocalDate createdAt) {
}Для создания записи клиент присылает только заголовок и текст, поэтому для входных данных нужен отдельный тип:
// src/main/java/com/example/blog/NewPost.java
package com.example.blog;
public record NewPost(String title, String body) {
}Хранилище
Пока базы данных нет, записи хранятся в памяти. Создайте класс src/main/java/com/example/blog/PostStore.java:
// src/main/java/com/example/blog/PostStore.java
package com.example.blog;
import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.ApplicationScoped;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
@ApplicationScoped
public class PostStore {
private final Map<Long, Post> posts = new ConcurrentHashMap<>();
private final AtomicLong nextId = new AtomicLong(1);
@PostConstruct
void init() {
add(new NewPost("Привет, Jakarta EE", "Первая запись блога."));
add(new NewPost("Сервлеты и фильтры", "Как запрос попадает в наш код."));
}
public List<Post> findAll() {
return new ArrayList<>(posts.values());
}
public Optional<Post> findById(long id) {
return Optional.ofNullable(posts.get(id));
}
public Post add(NewPost data) {
long id = nextId.getAndIncrement();
Post post = new Post(id, data.title(), data.body(), LocalDate.now());
posts.put(id, post);
return post;
}
}@ApplicationScoped просит сервер создать один объект хранилища на всё приложение, а @PostConstruct — вызвать init() сразу после создания. Подробно об этом — в главе про CDI. Пока важно одно: хранилище получают через @Inject, а не через new.
Ресурс с JSON
Перепишите PostResource так, чтобы он работал с хранилищем:
// src/main/java/com/example/blog/PostResource.java
package com.example.blog;
import jakarta.inject.Inject;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
@Path("/posts")
@Produces(MediaType.APPLICATION_JSON)
public class PostResource {
@Inject
PostStore store;
@GET
public List<Post> list() {
return store.findAll();
}
@GET
@Path("/{id}")
public Post show(@PathParam("id") long id) {
return store.findById(id).orElse(null);
}
@POST
@Consumes(MediaType.APPLICATION_JSON)
public Post create(NewPost data) {
return store.add(data);
}
}Метод create принимает NewPost: JSON из тела запроса превратится в объект сам.
curl http://localhost:8080/blog/api/posts/1
curl -H "Content-Type: application/json" -d '{"title":"JSON в Jakarta REST","body":"Записи превращаются в JSON."}' http://localhost:8080/blog/api/posts{"body":"Первая запись блога.","createdAt":"2026-10-04","id":1,"title":"Привет, Jakarta EE"}
{"body":"Записи превращаются в JSON.","createdAt":"2026-10-04","id":3,"title":"JSON в Jakarta REST"}
JSON-B по умолчанию сортирует поля по алфавиту, а LocalDate записывает в формате ISO.
Настройка JSON
Имена полей, порядок и формат дат меняют аннотациями:
// src/main/java/com/example/blog/Post.java
package com.example.blog;
import jakarta.json.bind.annotation.JsonbDateFormat;
import jakarta.json.bind.annotation.JsonbProperty;
import jakarta.json.bind.annotation.JsonbPropertyOrder;
import java.time.LocalDate;
@JsonbPropertyOrder({"id", "title", "body", "createdAt"})
public record Post(
long id,
String title,
String body,
@JsonbProperty("created_at") @JsonbDateFormat("dd.MM.yyyy") LocalDate createdAt) {
}{"id":1,"title":"Привет, Jakarta EE","body":"Первая запись блога.","created_at":"04.10.2026"}
| Аннотация | Что делает |
|---|---|
@JsonbProperty("name") | Другое имя поля в JSON |
@JsonbTransient | Не выводить поле |
@JsonbDateFormat("dd.MM.yyyy") | Формат даты |
@JsonbPropertyOrder | Порядок полей |
@JsonbNillable | Выводить поля со значением null |
Дальше в курсе используется простой вариант Post без аннотаций.
Частая ошибка
Отправить JSON без заголовка Content-Type: application/json. Команда curl -d без -H ставит тип application/x-www-form-urlencoded, и метод с @Consumes(MediaType.APPLICATION_JSON) его не примет:
HTTP/1.1 415 Unsupported Media Type
Код 415 означает, что сервер не умеет читать тело такого типа. Проверьте заголовок Content-Type у запроса.