Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

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.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.