Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

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 у запроса.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.