Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

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

SituationWhat happens
A field is nullIt is left out of the JSON: {"id":2,"title":"Only title"}
The JSON has an extra fieldIt is ignored
Malformed JSONResponse 400 Invalid JSON
No Content-Type: application/json headerResponse 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.

Thanks for reading!

Donate to the site — we work hard for you.