JSON на входе и выходе
REST API обменивается данными в формате JSON. Javalin не превращает объекты в JSON сам, а поручает это библиотеке Jackson, которую мы подключили в pom.xml. Ваша задача — описать данные классами Java и вызвать ctx.json(...) для ответа или ctx.bodyAsClass(...) для чтения запроса.
Модель данных
Пост блога опишем записью (record). Создайте два файла рядом с 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 — пост целиком, с номером. NewPost — то, что присылает клиент при создании: номер назначает сервер.
Хранилище в памяти
Пока базы данных нет, посты будут жить в словаре. Базу подключим в главе 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("Привет, Javalin", "Первый пост.", "Анна"));
create(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 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 и AtomicLong нужны, потому что Jetty обрабатывает запросы в нескольких потоках одновременно.
Ответ в JSON
Создайте хранилище в main и замените маршруты /posts:
// 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("Пост " + id + " не найден"));
ctx.json(post);
});
});ctx.json превращает объект в JSON и ставит заголовок Content-Type: application/json. NotFoundResponse из пакета io.javalin.http отвечает кодом 404 — подробнее в следующем уроке.
curl http://localhost:8080/posts/1{"id":1,"title":"Привет, Javalin","body":"Первый пост.","author":"Анна"}
JSON в запросе
ctx.bodyAsClass читает тело запроса и создаёт из него объект:
// 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 в Javalin","body":"Всю работу делает Jackson.","author":"Анна"}'HTTP/1.1 201 Created
Content-Type: application/json
{"id":3,"title":"JSON в Javalin","body":"Всю работу делает Jackson.","author":"Анна"}
Частая ошибка
По умолчанию Jackson отказывается читать JSON с полем, которого нет в классе. Запрос с лишним полем "tags" получает 500 Server Error, а в логе:
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")
Если лишние поля нужно молча пропускать, добавьте к записи аннотацию @JsonIgnoreProperties(ignoreUnknown = true) из пакета com.fasterxml.jackson.annotation.