JSON, records and @Serdeable
A REST API exchanges data as JSON. In Micronaut, turning objects into JSON and back is the job of Micronaut Serialization (serde for short, from serialize/deserialize). Unlike plain Jackson, it does not inspect classes through reflection at runtime: the reading and writing code is generated at compile time. So every class that goes out as JSON or comes in from JSON must be explicitly allowed with the @Serdeable annotation.
The article model
Records are a convenient way to describe data: they are short and immutable. Create two files:
// 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 is the article we send to the client. ArticleForm is what the client sends when creating one: there is no id, the server assigns it. The micronaut-serde-jackson module is already in pom.xml from the generator.
A JSON controller
Replace the contents of ArticleController. For now the articles live right in the controller, in memory:
// 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 means: build an ArticleForm from the request body. HttpResponse.created answers with status 201 and a location header holding the new article's address. When the Optional is empty, Micronaut returns 404 by itself.
Trying it out
curl -i -X POST http://localhost:8080/articles \
-H "Content-Type: application/json" \
-d '{"title":"Hello, Micronaut","body":"First article"}'HTTP/1.1 201 Created
location: /articles/1
content-type: application/json
{"id":1,"title":"Hello, Micronaut","body":"First article"}Now curl http://localhost:8080/articles returns the array [{"id":1,...}], and curl -i http://localhost:8080/articles/7 returns 404 Not Found.
Worth knowing
| Situation | What happens |
|---|---|
A field is null | It is left out of the JSON: {"id":2,"title":"Only title"} |
| The JSON has an extra field | It is ignored |
| Malformed JSON | Response 400 Invalid JSON |
No Content-Type: application/json header | Response 415 Unsupported Media Type |
Common mistake
If you forget @Serdeable on a record, the project still builds, but the request fails with a 500 error. The server log shows the reason:
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.Add @Serdeable to every class that passes through JSON. For third-party classes you cannot change, use @SerdeImport.