Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

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.

Thanks for reading!

Donate to the site — we work hard for you.