JSON and JSON-B
APIs usually exchange data as JSON. You do not have to write it by hand: Jakarta EE has the JSON-B specification (JSON Binding), which turns Java objects into JSON and back. Jakarta REST calls it by itself when a method is marked @Produces(MediaType.APPLICATION_JSON). So resource methods simply return objects.
Data model
A blog post is conveniently described as a record. JSON-B supports records since 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) {
}To create a post, the client sends only the title and text, so the input data needs its own type:
// src/main/java/com/example/blog/NewPost.java
package com.example.blog;
public record NewPost(String title, String body) {
}The store
There is no database yet, so posts are kept in memory. Create the class 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("Hello, Jakarta EE", "The first post of the blog."));
add(new NewPost("Servlets and filters", "How a request reaches our code."));
}
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 asks the server to create one store object for the whole application, and @PostConstruct asks it to call init() right after creation. The CDI chapter covers this in detail. For now one thing matters: you get the store through @Inject, not through new.
A resource with JSON
Rewrite PostResource to work with the store:
// 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);
}
}The create method accepts a NewPost: the JSON from the request body becomes an object by itself.
curl http://localhost:8080/blog/api/posts/1
curl -H "Content-Type: application/json" -d '{"title":"JSON in Jakarta REST","body":"Records turn into JSON."}' http://localhost:8080/blog/api/posts{"body":"The first post of the blog.","createdAt":"2026-10-04","id":1,"title":"Hello, Jakarta EE"}
{"body":"Records turn into JSON.","createdAt":"2026-10-04","id":3,"title":"JSON in Jakarta REST"}
By default JSON-B sorts fields alphabetically and writes a LocalDate in ISO format.
Tuning JSON
Field names, order and date format are changed with annotations:
// 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":"Hello, Jakarta EE","body":"The first post of the blog.","created_at":"04.10.2026"}
| Annotation | What it does |
|---|---|
@JsonbProperty("name") | A different field name in JSON |
@JsonbTransient | Leave the field out |
@JsonbDateFormat("dd.MM.yyyy") | Date format |
@JsonbPropertyOrder | Field order |
@JsonbNillable | Write fields whose value is null |
The rest of the course uses the plain Post without annotations.
Common mistake
Sending JSON without the Content-Type: application/json header. curl -d without -H sets the type application/x-www-form-urlencoded, and a method with @Consumes(MediaType.APPLICATION_JSON) does not accept it:
HTTP/1.1 415 Unsupported Media Type
Status 415 means the server cannot read a body of that type. Check the request's Content-Type header.