Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

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"}
AnnotationWhat it does
@JsonbProperty("name")A different field name in JSON
@JsonbTransientLeave the field out
@JsonbDateFormat("dd.MM.yyyy")Date format
@JsonbPropertyOrderField order
@JsonbNillableWrite 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.

Thanks for reading!

Donate to the site — we work hard for you.