JSON in and out
A REST API exchanges data in JSON format. Javalin does not turn objects into JSON by itself; it hands the job to the Jackson library that we added in pom.xml. Your job is to describe the data with Java classes and call ctx.json(...) for a response or ctx.bodyAsClass(...) to read a request.
The data model
A blog post is described with a record. Create two files next to Main.java:
// src/main/java/com/example/blog/Post.java
package com.example.blog;
public record Post(long id, String title, String body, String author) {
}// src/main/java/com/example/blog/NewPost.java
package com.example.blog;
public record NewPost(String title, String body, String author) {
}Post is the whole post with its number. NewPost is what the client sends when creating one: the server assigns the number.
An in-memory store
Until we have a database, posts live in a map. We connect the database in chapter 4.
// src/main/java/com/example/blog/PostStore.java
package com.example.blog;
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;
public class PostStore {
private final Map<Long, Post> posts = new ConcurrentHashMap<>();
private final AtomicLong nextId = new AtomicLong(1);
public PostStore() {
create(new NewPost("Hello, Javalin", "The first post.", "Anna"));
create(new NewPost("Routes", "How routes work.", "Oleg"));
}
public List<Post> findAll() {
return new ArrayList<>(posts.values());
}
public Optional<Post> findById(long id) {
return Optional.ofNullable(posts.get(id));
}
public Post create(NewPost data) {
long id = nextId.getAndIncrement();
Post post = new Post(id, data.title(), data.body(), data.author());
posts.put(id, post);
return post;
}
public boolean delete(long id) {
return posts.remove(id) != null;
}
}ConcurrentHashMap and AtomicLong are needed because Jetty handles requests in several threads at once.
A JSON response
Create the store in main and replace the /posts routes:
// src/main/java/com/example/blog/Main.java
PostStore store = new PostStore();
Javalin app = Javalin.create(config -> {
config.routes.get("/posts", ctx -> ctx.json(store.findAll()));
config.routes.get("/posts/{id}", ctx -> {
long id = ctx.pathParamAsClass("id", Long.class).get();
Post post = store.findById(id)
.orElseThrow(() -> new NotFoundResponse("Post " + id + " not found"));
ctx.json(post);
});
});ctx.json turns an object into JSON and sets the Content-Type: application/json header. NotFoundResponse from the io.javalin.http package answers with code 404 — more on it in the next lesson.
curl http://localhost:8080/posts/1{"id":1,"title":"Hello, Javalin","body":"The first post.","author":"Anna"}
JSON in a request
ctx.bodyAsClass reads the request body and creates an object from it:
// src/main/java/com/example/blog/Main.java
config.routes.post("/posts", ctx -> {
NewPost data = ctx.bodyAsClass(NewPost.class);
ctx.status(201).json(store.create(data));
});curl -i -X POST http://localhost:8080/posts \
-H "Content-Type: application/json" \
-d '{"title":"JSON in Javalin","body":"Jackson does the work.","author":"Anna"}'HTTP/1.1 201 Created
Content-Type: application/json
{"id":3,"title":"JSON in Javalin","body":"Jackson does the work.","author":"Anna"}
Common mistake
By default Jackson refuses to read JSON with a field the class does not have. A request with an extra "tags" field gets 500 Server Error, and the log says:
WARN io.javalin.Javalin - Uncaught exception
com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "tags"
(class com.example.blog.NewPost), not marked as ignorable (3 known properties: "title", "body", "author")
If extra fields should be skipped silently, add the @JsonIgnoreProperties(ignoreUnknown = true) annotation from the com.fasterxml.jackson.annotation package to the record.