JSON, записи и @Serdeable
REST API обменивается данными в формате JSON. В Micronaut за превращение объектов в JSON и обратно отвечает модуль Micronaut Serialization (коротко serde — от serialize/deserialize). В отличие от обычного Jackson, он не изучает классы через рефлексию при работе: код чтения и записи генерируется при компиляции. Поэтому каждый класс, который уходит в JSON или приходит из него, нужно явно разрешить аннотацией @Serdeable.
Модель статьи
Данные удобно описывать записями (record) — они короткие и неизменяемые. Создайте два файла:
// src/main/java/com/example/Article.java
package com.example;
import io.micronaut.serde.annotation.Serdeable;
@Serdeable
public record Article(Long id, String title, String body) {
}// src/main/java/com/example/ArticleForm.java
package com.example;
import io.micronaut.serde.annotation.Serdeable;
@Serdeable
public record ArticleForm(String title, String body) {
}Article — статья, которую мы отдаём клиенту. ArticleForm — то, что клиент присылает при создании: без id, его назначает сервер. Модуль micronaut-serde-jackson уже подключён генератором в pom.xml.
Контроллер с JSON
Замените содержимое ArticleController. Статьи пока храним прямо в контроллере, в памяти:
// src/main/java/com/example/ArticleController.java
package com.example;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Post;
import java.net.URI;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
@Controller("/articles")
public class ArticleController {
private final Map<Long, Article> articles = new ConcurrentHashMap<>();
private final AtomicLong nextId = new AtomicLong(1);
@Get
public Collection<Article> list() {
return articles.values();
}
@Get("/{id}")
public Optional<Article> show(Long id) {
return Optional.ofNullable(articles.get(id));
}
@Post
public HttpResponse<Article> create(@Body ArticleForm form) {
Article article = new Article(nextId.getAndIncrement(), form.title(), form.body());
articles.put(article.id(), article);
return HttpResponse.created(article, URI.create("/articles/" + article.id()));
}
}@Body говорит: собрать ArticleForm из тела запроса. HttpResponse.created отвечает статусом 201 и заголовком location с адресом новой статьи. Если Optional пустой, Micronaut сам вернёт 404.
Проверка
curl -i -X POST http://localhost:8080/articles \
-H "Content-Type: application/json" \
-d '{"title":"Привет, Micronaut","body":"Первая статья"}'HTTP/1.1 201 Created
location: /articles/1
content-type: application/json
{"id":1,"title":"Привет, Micronaut","body":"Первая статья"}Теперь curl http://localhost:8080/articles вернёт массив [{"id":1,...}], а curl -i http://localhost:8080/articles/7 — 404 Not Found.
Что ещё нужно знать
| Ситуация | Что происходит |
|---|---|
Поле равно null | В JSON его нет: {"id":2,"title":"Только заголовок"} |
| В JSON лишнее поле | Оно игнорируется |
| JSON с ошибкой | Ответ 400 Invalid JSON |
Нет заголовка Content-Type: application/json | Ответ 415 Unsupported Media Type |
Частая ошибка
Если забыть @Serdeable на записи, проект соберётся, но запрос упадёт с ошибкой 500. В журнале сервера будет причина:
Caused by: io.micronaut.core.beans.exceptions.IntrospectionException: No serializable introspection present for type Article.
Consider adding Serdeable. Serializable annotate to type Article. Alternatively if you are not in control of the project's
source code, you can use @SerdeImport(Article.class) to enable serialization of this type.Добавьте @Serdeable на каждый класс, который проходит через JSON. Для чужих классов, которые нельзя изменить, используют @SerdeImport.