Docs navigation
1 Introduction
Serialization 3.1.2-SNAPSHOT
On this page

Serialization

1 Introduction

Micronaut Serialization is a library that allows the serialization and deserialization of objects to common serialization formats like JSON.

It does so using build-time Bean Introspections that do not use reflection and allows using a variety of common annotation models including Jackson annotations, JSON-B annotations or BSON annotations.

Micronaut Serialization can be used to replace the use of Jackson Databind in a Micronaut application and allows serialization on top of a number of different encoding runtimes including Jackson Core, JSON-P, BSON, or CBOR.

1.1 Why Micronaut Serialization?

The goal of this project is to be an almost complete build-time replacement for Jackson Databind, that does not rely on reflection and has a smaller runtime footprint. The reasons to provide an alternative to Jackson are outlined below.

Memory Performance

Micronaut Serialization consumes less memory and has a much smaller runtime component. As a way of comparison Micronaut Serialization is a 380kb JAR file, compared to Jackson Databind which is well over 2mb. This results in a reduction of 5MB in terms of image size for native image builds.

The elimination of reflection and smaller footprint also results in reduced runtime memory consumption.

Runtime Performance

The UserBeanSerdeBenchmark JMH benchmark compares Jackson Databind, Jackson Databind with Blackbird, Micronaut Serialization using generated serializers/deserializers, and Micronaut Serialization with generated serializers/deserializers disabled. A full local UserBeanSerdeBenchmark run on OpenJDK 25 used 3 forks, 5 warmup iterations, and 5 measurement iterations with 1-second iterations and -prof gc.

Operation Jackson Databind Jackson Databind Blackbird Micronaut Serialization generated Micronaut Serialization runtime

Serialize throughput

386,554 ops/s

389,268 ops/s

553,140 ops/s

418,342 ops/s

Deserialize average time

3,366 ns/op

3,213 ns/op

3,052 ns/op

3,129 ns/op

Round-trip average time

6,469 ns/op

6,398 ns/op

4,923 ns/op

5,379 ns/op

Serialize allocation

6,176 B/op

6,176 B/op

2,968 B/op

2,990 B/op

UserBeanSerdeBenchmark local results

In this run, generated Micronaut Serialization had the highest serialization throughput, about 43.1% faster than Jackson Databind and about 32.2% faster than the runtime fallback, while also allocating less than half as many bytes per serialized payload. It was also the fastest deserialization path, about 9.3% faster than Jackson Databind and about 2.5% faster than the runtime fallback, and the fastest combined serialize-and-deserialize round trip, about 23.9% faster than Jackson Databind and about 8.5% faster than the runtime fallback. The runtime fallback remained slower than generated Micronaut Serialization across all measured operations, which is expected because it uses runtime serializer/deserializer selection instead of generated classes.

Security

Unlike Jackson, you cannot serialize or deserialize arbitrary objects to JSON. Allowing arbitrary serialization is often a source of security issues in modern applications. Instead with Micronaut Serialization to allow a type to be serialized or deserialized you must do one of the following:

  1. Declare the @Serdeable annotation at the type level in your source code to allow the type to be serialized or deserialized.

  2. If you cannot modify the source code and the type is an external type you can use @SerdeImport to import the type. Note that with this approach only public members are considered.

  3. Define a bean of type Serializer for serialization and/or a bean of type Deserializer for deserialization.

Type Safety

Jackson provides an annotation-based programming model that includes many rules developers need to be aware of and can lead to runtime exceptions if these rules are violated.

Micronaut Serialization adds compile-time checking for correctness when using JSON binding annotations.

Runtime Portability

Micronaut Serialization decouples the runtime from the actual source code level annotation model whilst Jackson is coupled to Jackson annotations. This means you can use the same runtime, but choose whether to use Jackson annotations, JSON-B annotations or BSON annotations

This leads to less memory consumption since there is no need to have multiple JSON parsers and reflection-based meta-models if you using both JSON in your webtier plus a document database like MongoDB.

2 Release History

For this project, you can find a list of releases (with release notes) here:

3 Quick Start

There are a number of ways to use Micronaut Serialization including a choice of annotation-model and runtime.

The first step however is configure the necessary annotation processor dependency:

annotationProcessor("io.micronaut.serde:micronaut-serde-processor")
Note
For Kotlin, add the micronaut-serde-processor dependency in kapt or ksp scope, and for Groovy add micronaut-serde-processor in compileOnly scope.

You should then choose a combination of Annotation-based programming model and runtime implementation that you desire.

3.1 Jackson Annotations & Jackson Core

To replace Jackson Databind, but continue using Jackson Annotations as a programming model and Jackson Core as a runtime replace the micronaut-jackson-databind module in your application with micronaut-serde-jackson.

Add the following artifact to the dependencies block:

implementation("io.micronaut.serde:micronaut-serde-jackson")

With the correct dependencies in place you can now define an object to be serialized:

Tip
If you don’t want to add a Micronaut Serialization annotation then you can also add a type-level Jackson annotation like @JsonClassDescription, @JsonRootName or @JsonTypeName

Once you have a type that can be serialized and deserialized you can use the ObjectMapper interface to do so:

package example;

import io.micronaut.serde.ObjectMapper;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Test;
import java.io.IOException;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

@MicronautTest
public class BookTest {

    @Test
    void testWriteReadBook(ObjectMapper objectMapper) throws IOException {
        String result = objectMapper.writeValueAsString(new Book("The Stand", 50));

        Book book = objectMapper.readValue(result, Book.class);
        assertNotNull(book);
        assertEquals(
                "The Stand", book.getTitle()
        );
        assertEquals(50, book.getQuantity());
    }
}

3.2 JSON-B Annotations & JSON-P

To completely remove all dependencies on Jackson Databind and use JSON-B annotations in your source code combined with JSON-P at runtime, replace micronaut-jackson-databind with Micronaut Serialization.

Micronaut Serialization provides three Jakarta JSON artifacts with different purposes:

  • micronaut-serde-jsonp is the existing JSON-P stream integration backed by Eclipse Parsson.

  • micronaut-serde-jsonp-impl is the Micronaut-native JSON-P provider. It implements jakarta.json.spi.JsonProvider without depending on Parsson and does not use reflection.

  • micronaut-serde-jsonb is the Micronaut Serialization backed JSON-B runtime provider. It implements jakarta.json.bind.spi.JsonbProvider; JSON-B compatibility behavior may use reflection only as an isolated fallback when Micronaut introspection and serialization metadata cannot satisfy a spec-required runtime type.

Add the following artifact to the dependencies block:

implementation("io.micronaut.serde:micronaut-serde-jsonp")

Use the Micronaut-native JSON-P provider when your application or library calls the Jakarta JSON-P provider APIs directly:

implementation("io.micronaut.serde:micronaut-serde-jsonp-impl")

Use the JSON-B runtime provider when your application or library calls JsonbBuilder or JsonbProvider:

implementation("io.micronaut.serde:micronaut-serde-jsonb")

The providers are loaded through the standard Jakarta service loader files:

  • META-INF/services/jakarta.json.spi.JsonProvider

  • META-INF/services/jakarta.json.bind.spi.JsonbProvider

Warning
If your third-party dependencies have direct dependencies on Jackson Databind it may not be an option to omit it.

With the correct dependencies in place you can now define an object to be serialized:

Once you have a type that can be serialized and deserialized you can use the ObjectMapper interface to do so:

package example;

import io.micronaut.serde.ObjectMapper;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Test;
import java.io.IOException;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

@MicronautTest
public class BookTest {

    @Test
    void testWriteReadBook(ObjectMapper objectMapper) throws IOException {
        String result = objectMapper.writeValueAsString(new Book("The Stand", 50));

        Book book = objectMapper.readValue(result, Book.class);
        assertNotNull(book);
        assertEquals(
                "The Stand", book.getTitle()
        );
        assertEquals(50, book.getQuantity());
    }
}

3.3 BSON Annotations and BSON

To completely remove all dependencies on Jackson and use BSON annotations in your source code combined with BSON at a runtime you should replace the micronaut-jackson-databind and micronaut-jackson-core modules in your application with micronaut-serde-bson.

Add the following artifact to the dependencies block:

implementation("io.micronaut.serde:micronaut-serde-bson")
Warning
If your third-party dependencies have direct dependencies on Jackson Databind it may not be an option to omit it.

With the correct dependencies in place you can now define an object to be serialized:

Once you have a type that can be serialized and deserialized you can use the ObjectMapper interface to do so:

package example;

import io.micronaut.serde.ObjectMapper;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Test;
import java.io.IOException;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

@MicronautTest
public class BookTest {

    @Test
    void testWriteReadBook(ObjectMapper objectMapper) throws IOException {
        String result = objectMapper.writeValueAsString(new Book("The Stand", 50));

        Book book = objectMapper.readValue(result, Book.class);
        assertNotNull(book);
        assertEquals(
                "The Stand", book.getTitle()
        );
        assertEquals(50, book.getQuantity());
    }
}

3.4 CBOR

To serialize and deserialize objects as CBOR (Concise Binary Object Representation), add the following dependency:

implementation("io.micronaut.serde:micronaut-serde-jackson-cbor")
Note
micronaut-serde-jackson-cbor uses Jackson’s streaming CBOR factory (jackson-dataformat-cbor) only as a token parser/generator. Object mapping is performed by Micronaut Serialization’s build-time serializers and deserializers. Jackson Databind is not used (the dataformat module’s transitive databind dependency is excluded).
Tip
Adding this module also brings micronaut-serde-jackson for the shared streaming encoder/decoder bridge. When both are present, Jackson remains the primary JSON JsonMapper. Inject CborObjectMapper for CBOR. JSON-only applications that do not depend on micronaut-serde-jackson-cbor never load CBOR beans.

With the dependency in place you can define an object to be serialized:

Use CborObjectMapper to read and write CBOR bytes:

package example;

import io.micronaut.serde.cbor.CborObjectMapper;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Test;

import java.io.IOException;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

@MicronautTest
public class BookTest {

    @Test
    void testWriteReadBook(CborObjectMapper cborObjectMapper) throws IOException {
        byte[] bytes = cborObjectMapper.writeValueAsBytes(new Book("The Stand", 50));
        assertNotNull(bytes);
        assertTrue(bytes.length > 0);
        // CBOR map major type, not JSON text
        assertTrue((bytes[0] & 0xE0) == 0xA0);

        Book book = cborObjectMapper.readValue(bytes, Book.class);
        assertNotNull(book);
        assertEquals("The Stand", book.getTitle());
        assertEquals(50, book.getQuantity());
    }
}

Coexistence with JSON

Need Inject / use

Default JSON

JsonMapper / ObjectMapper (primary = Jackson when present)

CBOR

CborObjectMapper or HTTP media type application/cbor

Both formats share the same @Serdeable types and generated serializers.

Binary data

For CBOR, byte[] defaults to native byte strings (RFC 8949 major type 2), not the global JSON-legacy numeric-array encoding. This is controlled by micronaut.serde.cbor.write-binary-as-array (default false) and does not change JSON behaviour.

To force numeric arrays for CBOR:

micronaut.serde.cbor.write-binary-as-array=true

HTTP

The module registers a message body handler for application/cbor. Controllers and clients can use:

@Post(uri = "/books", processes = CborMediaTypes.APPLICATION_CBOR)
Book save(@Body Book book) {
    return book;
}

The constant CborMediaTypes#APPLICATION_CBOR is provided for convenience. When Micronaut Core adds MediaType.APPLICATION_CBOR, that constant will remain a stable alias for the same media type string (application/cbor).

Security and limits

CBOR parsing and writing honour micronaut.serde.maximum-nesting-depth (and related stream limits) to bound stack usage on nested structures. Prefer keeping limits enabled for untrusted input. Very large byte strings are limited by available heap; apply HTTP body size limits at the server as usual.

Streaming factory features

Optional CBOR and stream features map to Jackson’s streaming configuration:

micronaut.serde.cbor.cbor-write-features.WRITE_MINIMAL_INTS=true
Note
These control token encoding details (for example minimal integer width). They do not implement full canonical/deterministic CBOR (sorted map keys, etc.).

Limitations and roadmap

  • Map keys are text strings (integer map keys are not supported without Serde SPI changes).

  • CBOR tags (date/time, bigfloats, etc.) are not applied; temporals use existing Micronaut Serde encodings.

  • Indefinite-length encoding follows the streaming library defaults.

  • Full deterministic/canonical CBOR is not a built-in mode.

  • Reactive parsing (createReactiveParser) buffers before parsing and splits chunks with a JSON-oriented lexer, so it is only reliable when each byte[] chunk is a complete CBOR value. Use the byte-array or stream APIs for chunked input.

  • byte[] read into a JsonNode tree or an untyped Object becomes a base64 string in the tree (the tree representation Micronaut Serde uses for binary) and a byte[] in the untyped case.

3.5 TOML

TOML serialization support is provided by the Micronaut TOML project through the micronaut-toml-serde module.

See the Micronaut TOML serialization documentation for dependency setup, the named TOML ObjectMapper bean, source-backed examples, mapper configuration, and TOML output layout.

3.6 Java Properties

Micronaut Serialization includes support for reading and writing Java *.properties documents with the micronaut-serde-properties module.

Add the following artifact to the dependencies block:

implementation("io.micronaut.serde:micronaut-serde-properties")

The *.properties mapper is exposed as a named ObjectMapper bean. Inject it with the properties qualifier when you want to read or write *.properties data:

import io.micronaut.serde.ObjectMapper;
import io.micronaut.serde.properties.PropertiesMapper;
import jakarta.inject.Named;

class BookService {
    private final ObjectMapper propertiesMapper;

    BookService(@Named(PropertiesMapper.NAME) ObjectMapper propertiesMapper) {
        this.propertiesMapper = propertiesMapper;
    }
}

Once you have a type that can be serialized and deserialized, the mapper flattens object paths into property keys:

book.title=The Stand
book.authors[0].name=Stephen King
book.authors[0].age=60
book.authors[1].name=JRR Tolkien
book.authors[1].age=81

By default, arrays use zero-based bracketed indexes such as authors[0]. You can configure one-based dotted indexes instead:

micronaut.serde.format.properties.array-index-style=DOTTED

With dotted indexes, the same array paths are written as:

book.authors.1.name=Stephen King
book.authors.2.name=JRR Tolkien
Note
Java *.properties documents require keys. Root objects can be written as properties, but root arrays and root scalar values cannot be written directly.

3.7 XML

To serialize and deserialize XML with Micronaut Serialization, choose either the JDK StAX runtime or the Woodstox runtime. Both provide a named XML ObjectMapper bean and use the same compile-time serialization metadata as the JSON, JSON-P, and BSON runtimes.

For the StAX implementation included with the JDK, add the following artifact to the dependencies block:

implementation("io.micronaut.serde:micronaut-serde-stax-xml")

For Woodstox, use the following artifact instead. It includes the StAX integration transitively and selects Woodstox as the XML input and output factory implementation:

implementation("io.micronaut.serde:micronaut-serde-woodstox-xml")

If your source code uses Jackson XML annotations such as @JacksonXmlProperty or @JacksonXmlElementWrapper, keep the Jackson XML annotation types on the compile classpath. Micronaut Serialization maps those annotations at compilation time; XML serialization still goes through Micronaut Serialization’s named XML mapper rather than a Jackson Databind XmlMapper.

Jakarta XML Binding annotations

Micronaut Serialization also recognizes the common Jakarta XML Binding 4 annotations at compile time. Add the annotation API to the compile classpath only; no JAXB runtime or JAXBContext is required:

compileOnly("jakarta.xml.bind:jakarta.xml.bind-api")

@XmlRootElement, @XmlType, @XmlAccessorOrder, @XmlAccessorType, and @XmlEnum automatically make a type serdeable. Property-only JAXB annotations require the containing type to be explicitly serdeable or to have one of those class-level annotations. The Supported members column is exhaustive; unlisted members are ignored.

Annotation Supported members XML behavior

@XmlRootElement

name, namespace

Makes the type serdeable and sets the document root name and namespace. The default name is derived by decapitalizing the class name; default namespace does not add an explicit namespace.

@XmlElement

name, namespace, defaultValue, nillable, type

Changes the property element name and namespace. Collections and arrays use inline repeated elements by default. defaultValue applies only when an element is present but empty; missing, whitespace-only, and xsi:nil elements retain their normal behavior. nillable = true writes null elements and collection entries as xsi:nil="true"; false omits them. type selects a compatible scalar or collection-item serde through shared serde type metadata. required is ignored.

@XmlAttribute

name, namespace

Writes and reads the property as an attribute, using the configured name and namespace. Attributes are ordered before child elements. required is ignored.

@XmlElementWrapper

name, namespace, nillable

Wraps a collection or array in an XML element. Combine it with @XmlElement to set the item name and namespace. nillable = true writes a null collection as an xsi:nil="true" wrapper; false omits it. Empty non-null collections write an empty wrapper. required is ignored.

@XmlElements

value (@XmlElement choices: name, type)

Declares unwrapped polymorphic element choices. The element name selects the declared subtype during deserialization and the runtime subtype selects its element during serialization. Each choice’s namespace and required members are ignored.

@XmlElementRef

name, type

Declares one unwrapped polymorphic element reference. The name and type select the XML element and subtype. namespace and required are ignored. JAXBElement references are not supported.

@XmlElementRefs

value (@XmlElementRef choices: name, type)

Declares multiple unwrapped polymorphic element references. Each choice behaves as @XmlElementRef; namespace and required are ignored.

@XmlSeeAlso

value

Registers the listed subtypes for serde processing and subtype resolution. It does not add an XML type discriminator; combine it with @XmlElements, @XmlElementRef, or @XmlElementRefs when the element name selects a subtype.

@XmlValue

None

Writes the property as direct text content of its owning element. At most one value property is supported.

@XmlTransient

None

Ignores the annotated field or property. On a type, it prevents JAXB auto-binding unless a Micronaut or Jackson annotation explicitly binds the type.

@XmlType

propOrder

Makes the type serdeable and applies a non-default property order. name, namespace, factoryClass, and factoryMethod are ignored.

@XmlAccessorOrder

value = ALPHABETICAL

Makes the type serdeable and orders properties alphabetically. UNDEFINED leaves normal property ordering in effect.

@XmlAccessorType

value

Makes the type serdeable and selects field or property introspection. FIELD includes all fields, PROPERTY and PUBLIC_MEMBER use public JavaBean properties, and NONE includes only JAXB-annotated members.

@XmlID

None

Marks a String property as the identity used by @XmlIDREF references. The ID keeps its normal element or attribute representation.

@XmlIDREF

None

Writes an object reference as the target’s @XmlID string. Deserialization of object references, forward references, and collections of references is not supported.

@XmlEnum

None

Makes the enum serdeable. value (the schema base type) is ignored.

@XmlEnumValue

value

Sets the serialized and deserialized lexical value for the enum constant.

Names, transient properties, ordering, enum lexical values, and type overrides use shared serde metadata and can therefore affect other format implementations. JAXB defaults and nil policies are XML-only. Explicit Micronaut or Jackson configuration wins when it conflicts with JAXB metadata. Unsupported JAXB features include package @XmlSchema, adapters, JAXBElement, schema generation, and JAXBContext; unsupported annotation members include all required flags, @XmlType.name, @XmlType.namespace, @XmlType.factoryClass, @XmlType.factoryMethod, and @XmlEnum.value.

With the correct dependencies in place you can define an XML-serializable type:

Supported XML Annotations

Micronaut Serialization supports every annotation in the tools.jackson.dataformat.xml.annotation package. The standard Jackson @JsonRootName and @JsonProperty annotations also supply names and namespaces used by the XML mapper. The supported XML-specific members are described below.

Annotation Supported members XML behavior

@JacksonXmlProperty

isAttribute, localName, namespace

By default, the property is an XML element. Set isAttribute = true to write and read it as an attribute. localName changes the element or attribute name, and namespace supplies its namespace URI.

@JacksonXmlElementWrapper

useWrapping, localName, namespace

Controls the container element for an array or collection. Wrapping is enabled by default and uses the property name when localName is empty. Set useWrapping = false to write entries directly under the parent; localName and namespace configure the wrapper when wrapping is enabled.

@JacksonXmlRootElement

localName, namespace

Changes the document root element name and namespace. It affects only the root value. This annotation is deprecated in Jackson 3; prefer @JsonRootName unless the XML root must differ from other formats.

@JacksonXmlText

value

With value = true (the default), writes and reads the property as direct text inside its owning element. Attributes may be present on that element. Set value = false to retain normal property-element handling.

@JacksonXmlCData

value

With value = true (the default), writes String properties and String collection entries as CDATA. It can be combined with @JacksonXmlText for direct CDATA content. Deserialization accepts both CDATA and ordinary character data. Set value = false to use normal escaped text.

@JsonRootName

value, namespace

value changes the document root element name and namespace supplies its namespace URI.

@JsonProperty

value, namespace

For XML, value changes the property element or attribute name and namespace supplies its namespace URI. It can be combined with @JacksonXmlProperty(isAttribute = true) to create a named, namespaced attribute. Other @JsonProperty behavior is shared with the non-XML runtimes.

An empty localName or value keeps the inferred property or type name. An empty namespace means that no explicit namespace URI is applied.

The XML mapper is registered as a named bean in both runtimes. Inject it with @Named using XmlObjectMapper.XML_MAPPER_NAME or the literal name xml:

package example;

import io.micronaut.serde.ObjectMapper;
import io.micronaut.serde.xml.XmlObjectMapper;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

@MicronautTest
public class BookTest {

    @Inject
    @Named(XmlObjectMapper.XML_MAPPER_NAME)
    ObjectMapper xmlMapper;

    @Test
    void testWriteReadBook() throws IOException {
        String result = xmlMapper.writeValueAsString(new Book(
            "978-0307743688",
            "The Stand",
            List.of("Stephen King")
        ));

        assertEquals(
            "<book isbn=\"978-0307743688\"><title>The Stand</title><authors><author>Stephen King</author></authors></book>",
            result
        );

        Book book = xmlMapper.readValue(result, Book.class);
        assertNotNull(book);
        assertEquals("978-0307743688", book.getIsbn());
        assertEquals("The Stand", book.getTitle());
        assertEquals(List.of("Stephen King"), book.getAuthors());
    }

    @Test
    void testWriteReadJaxbBook() throws IOException {
        JaxbBook input = new JaxbBook();
        input.isbn = "978-0307743688";
        input.title = "The Stand";
        input.authors = List.of("Stephen King");

        String result = xmlMapper.writeValueAsString(input);

        assertEquals(
            "<book isbn=\"978-0307743688\"><title>The Stand</title><subtitle xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:nil=\"true\"></subtitle><author>Stephen King</author></book>",
            result
        );

        JaxbBook book = xmlMapper.readValue(result, JaxbBook.class);
        assertEquals(input.isbn, book.isbn);
        assertEquals(input.title, book.title);
        assertEquals("Untitled", book.subtitle);
        assertEquals(input.authors, book.authors);
    }
}

XML-specific behavior can be configured under micronaut.serde.format.xml:

micronaut.serde.format.xml.repairing-namespaces=true
micronaut.serde.format.xml.automatic-empty-elements=false
micronaut.serde.format.xml.xml-read-features.EMPTY_ELEMENT_AS_NULL=true

repairing-namespaces defaults to true and lets the XML writer declare namespace prefixes when a property or root element has a namespace. Set it to false only when the XML stream writer is expected to receive already-valid namespace bindings.

automatic-empty-elements defaults to false. Enable it to allow the XML output factory to write self-closing empty elements such as <book/> when the configured StAX implementation supports that feature.

xml-read-features.EMPTY_ELEMENT_AS_NULL defaults to false. Enable it when empty XML elements should deserialize as null instead of an empty string or empty bean.

4 Jackson Annotations

Micronaut Serialization supports a subset of the available Jackson Annotations.

The primary difference is Micronaut Serialization uses build-time Bean Introspections, this means that only accessible getters and setters (and Java 17 records) are supported and @JsonAutoDetect cannot be used to customize mapping.

Tip
You can however, enable fields to be included using AccessKind field. See the "Bean Fields" section of the Bean Introspections docs.

The full list of supported Jackson annotations and members is described in the table below.

Note
If an unsupported annotation or member is used, a compilation error will result.
Jackson Annotation Supported Notes

@JsonAlias

@JacksonInject

@JsonAnyGetter

unsupported members: enabled

@JsonAnySetter

unsupported members: enabled

@JsonAutoDetect

@JsonBackReference

@JsonClassDescription

@JsonCreator

@JsonEnumDefaultValue

supported for enum properties using @JsonFormat(with = JsonFormat.Feature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE)

@JsonFilter

supported only on types, implement the io.micronaut.serde.PropertyFilter interface

@JsonFormat

@JsonGetter

@JsonIdentityInfo

@JsonIdentityReference

@JsonIgnore

unsupported members: enabled

@JsonIgnoreProperties

@JsonIgnoreType

@JsonInclude

unsupported members: contentFilter, valueFilter

@JsonKey

@JsonManagedReference

@JsonMerge

Supported for explicit property-level merge during ObjectMapper.updateValue and updateValueFromTree; no public readerForUpdating API is provided.

@JsonProperty

@JsonPropertyDescription

@JsonPropertyOrder

@JsonRawValue

Not supported for security reasons

@JsonRootName

@JsonSetter

unsupported members: null & contentNull

@JsonSubTypes

@JsonTypeId

@JsonTypeInfo

Only CLASS, MINIMAL_CLASS, NAME and DEDUCTION for use.

@JsonTypeName

@JsonUnwrapped

unsupported members: enabled

@JsonValue

unsupported members: value

@JsonView

@JsonMerge

The @JsonMerge annotation enables merge behavior when updating an existing mutable object with ObjectMapper.

Without @JsonMerge, an incoming nested object replaces the current property value. With @JsonMerge, Micronaut Serialization updates the existing nested value when possible: JSON fields present in the update replace matching fields, and fields absent from the update keep their current values.

For example, this release configuration uses @JsonMerge on a nested deployment window and a labels map:

package example;

import com.fasterxml.jackson.annotation.JsonMerge;
import io.micronaut.serde.annotation.Serdeable;

import java.util.LinkedHashMap;
import java.util.Map;

@Serdeable
public class ReleaseConfiguration {
    private String service = "";
    private String owner = "";
    @JsonMerge
    private DeploymentWindow deploymentWindow = new DeploymentWindow();
    @JsonMerge
    private Map<String, String> labels = new LinkedHashMap<>();

    public String getService() {
        return service;
    }

    public void setService(String service) {
        this.service = service;
    }

    public String getOwner() {
        return owner;
    }

    public void setOwner(String owner) {
        this.owner = owner;
    }

    public DeploymentWindow getDeploymentWindow() {
        return deploymentWindow;
    }

    public void setDeploymentWindow(DeploymentWindow deploymentWindow) {
        this.deploymentWindow = deploymentWindow;
    }

    public Map<String, String> getLabels() {
        return labels;
    }

    public void setLabels(Map<String, String> labels) {
        this.labels = labels;
    }

    @Serdeable
    public static class DeploymentWindow {
        private String day = "";
        private String timeZone = "";

        public String getDay() {
            return day;
        }

        public void setDay(String day) {
            this.day = day;
        }

        public String getTimeZone() {
            return timeZone;
        }

        public void setTimeZone(String timeZone) {
            this.timeZone = timeZone;
        }
    }
}

The update JSON can then include only the values that should change:

package example;

import io.micronaut.core.type.Argument;
import io.micronaut.serde.ObjectMapper;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;

@MicronautTest
public class JsonMergeExampleTest {

    @Test
    void testMergeNestedReleaseConfiguration(ObjectMapper objectMapper) throws IOException {
        ReleaseConfiguration release = new ReleaseConfiguration();
        release.setService("checkout");
        release.setOwner("platform");

        ReleaseConfiguration.DeploymentWindow window = new ReleaseConfiguration.DeploymentWindow();
        window.setDay("Friday");
        window.setTimeZone("UTC");
        release.setDeploymentWindow(window);

        objectMapper.updateValue(
            release,
            Argument.of(ReleaseConfiguration.class),
            """
            {
              "owner": "growth",
              "deploymentWindow": {
                "day": "Tuesday"
              }
            }
            """.getBytes(StandardCharsets.UTF_8)
        );

        assertEquals("growth", release.getOwner());
        assertSame(window, release.getDeploymentWindow());
        assertEquals("Tuesday", release.getDeploymentWindow().getDay());
        assertEquals("UTC", release.getDeploymentWindow().getTimeZone());
    }

    @Test
    void testMergeReleaseLabels(ObjectMapper objectMapper) throws IOException {
        ReleaseConfiguration release = new ReleaseConfiguration();
        release.setLabels(new java.util.LinkedHashMap<>(Map.of(
            "environment", "production",
            "region", "us-east"
        )));

        objectMapper.updateValue(
            release,
            Argument.of(ReleaseConfiguration.class),
            """
            {
              "labels": {
                "version": "2026.06",
                "region": "eu-west"
              }
            }
            """.getBytes(StandardCharsets.UTF_8)
        );

        assertEquals("production", release.getLabels().get("environment"));
        assertEquals("eu-west", release.getLabels().get("region"));
        assertEquals("2026.06", release.getLabels().get("version"));
    }
}

In the nested object case, the update changes the deployment day but keeps the existing time zone. If the deploymentWindow property is not annotated with @JsonMerge, the incoming object replaces the current one and the absent time zone value is lost. In the map case, incoming labels update matching keys and add new keys while keys absent from the update remain in the map.

@JsonMerge is explicit and property-scoped. It is supported for mutable readable bean properties, mutable maps, mutable collections, and array properties. Immutable, creator-only, builder-only, and record-like values cannot be updated in place. Explicit JSON null values follow the normal null handling rules instead of attempting a merge. Micronaut Serialization does not provide a public Jackson-style readerForUpdating API; use ObjectMapper.updateValue(…​) or updateValueFromTree(…​) to update existing values.

In addition, limited support for 3 jackson-databind annotations is included to allow portability for cases where both support for jackson-databind and Micronaut Serialization is required:

Annotation Notes

@JsonNaming

Only with the built-in naming strategies

@JsonSerialize

Only the as member

@JsonDeserialize

Only the as member

Note that when using these annotations it is recommended that you make jackson-databind a compileOnly dependency since it is not needed at runtime. For example for Gradle:

jackson-databind as compileOnly scope
compileOnly("com.fasterxml.jackson.core:jackson-databind")

or Maven:

jackson-databind as provided scope
<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
  <scope>provided</scope>
</dependency>

@JsonView on controllers

The Micronaut HTTP server supports declaring the Jackson @JsonView annotation on controllers to configure a subset of fields to be serialized. micronaut-serialization supports this feature when enabled through the jackson.json-view.enabled or micronaut.serde.json-view-enabled configuration property.

4.1 Custom Property Filters

Custom property filters can be written by implementing the PropertyFilter interface.

For example, given the following class:

A custom property filter can be defined as follows:

The filter omits the name field when the preferredName field is set:

package example;

import io.micronaut.serde.ObjectMapper;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Test;

import java.io.IOException;

import static org.junit.jupiter.api.Assertions.assertEquals;

@MicronautTest
public class PersonFilterTest {

    @Test
    void testWritePersonWithoutPreferredName(ObjectMapper objectMapper) throws IOException {
        String result = objectMapper.writeValueAsString(new Person("Adam", null));
        assertEquals("{\"name\":\"Adam\"}", result);
    }

    @Test
    void testWritePersonWithPreferredName(ObjectMapper objectMapper) throws IOException {
        String result = objectMapper.writeValueAsString(new Person("Adam", "Ad"));
        assertEquals("{\"preferredName\":\"Ad\"}", result);
    }
}

5 Jackson Runtime Configuration

When using the micronaut-serde-jackson module, the underlying Jackson Core JsonFactory can be tuned through configuration under the micronaut.serde.jackson prefix. No configuration is required by default; these properties exist to adjust Jackson’s parsing and generation behavior when your payloads need it.

The following properties are supported:

Property Type Description

micronaut.serde.jackson.pretty-print

boolean

Write indented, human-readable JSON output. Defaults to false.

micronaut.serde.jackson.json-read-features

Map

JsonReadFeature overrides for JSON-specific parsing behavior, such as accepting comments or single quotes.

micronaut.serde.jackson.json-write-features

Map

JsonWriteFeature overrides for JSON-specific generation behavior, such as escaping non-ASCII characters.

micronaut.serde.jackson.stream-read-features

Map

Format-independent StreamReadFeature overrides, such as strict duplicate detection or including source content in error locations.

micronaut.serde.jackson.stream-write-features

Map

Format-independent StreamWriteFeature overrides, such as writing BigDecimal values in plain notation.

micronaut.serde.jackson.json-factory-features

Map

TokenStreamFactory.Feature overrides for factory-level behavior, such as property-name interning and canonicalization.

Each map uses the Jackson feature constant name as the key and a boolean as the value. For example:

micronaut.serde.jackson.pretty-print=false
micronaut.serde.jackson.json-read-features.ALLOW_JAVA_COMMENTS=true
micronaut.serde.jackson.json-read-features.ALLOW_SINGLE_QUOTES=true
micronaut.serde.jackson.json-write-features.ESCAPE_NON_ASCII=true
micronaut.serde.jackson.stream-read-features.INCLUDE_SOURCE_IN_LOCATION=true
micronaut.serde.jackson.stream-write-features.WRITE_BIGDECIMAL_AS_PLAIN=true

Features that are not configured keep their Jackson Core defaults.

Note
The Jackson Core defaults are already tuned for performance on modern JVMs. In particular, StreamReadFeature.USE_FAST_DOUBLE_PARSER is enabled by default, and enabling StreamWriteFeature.USE_FAST_DOUBLE_WRITER measured slower than the built-in JDK Double.toString implementation on recent JDKs in the project’s local benchmarks, so it is best left disabled.

6 JSON-B Support

Micronaut Serialization supports JSON-B annotations through the compile-time serializer metadata and provides a JSON-B runtime provider in the micronaut-serde-jsonb module.

If you only use JSON-B annotations on classes serialized through Micronaut Serialization APIs, include jakarta.json.bind-api as a compile-only dependency. If you need the jakarta.json.bind.Jsonb runtime API, JSON-B serializers, JSON-B deserializers, adapters, visibility strategies, or programmatic JsonbConfig, add the Micronaut JSON-B runtime provider:

implementation("io.micronaut.serde:micronaut-serde-jsonb")

The context-created Jsonb bean uses generated Micronaut Serialization metadata by default and automatically enables the JSON-B compatibility provider when runtime JSON-B customizations require it.

6.1 JSON-B Annotations

Micronaut Serialization supports JSON-B annotations for generated serialization metadata and JSON-B runtime compatibility features.

package example;

import io.micronaut.serde.annotation.Serdeable;
import jakarta.json.bind.annotation.JsonbCreator;
import jakarta.json.bind.annotation.JsonbProperty;

@Serdeable // 
public class Book {
    private final String title;
    @JsonbProperty("qty") // 
    private final int quantity;

    @JsonbCreator // 
    public Book(String title, int quantity) {
        this.title = title;
        this.quantity = quantity;
    }

    public String getTitle() {
        return title;
    }

    public int getQuantity() {
        return quantity;
    }
}
JSON-B API Supported Notes

@JsonbCreator

Yes

@JsonbDateFormat

Yes

@JsonbNillable

Yes

@JsonbNumberFormat

Yes

@JsonbProperty

Yes

@JsonbPropertyOrder

Yes

@JsonbTransient

Yes

@JsonbTypeAdapter

Yes

Supported by the JSON-B runtime compatibility provider.

@JsonbTypeDeserializer

Yes

Supported by the JSON-B runtime compatibility provider.

@JsonbTypeInfo

Yes

Subtype aliases are read from @JsonbSubtype declarations.

@JsonbSubtype

Yes

Used with @JsonbTypeInfo.

@JsonbTypeSerializer

Yes

Supported by the JSON-B runtime compatibility provider.

@JsonbVisibility

Yes

Supported by the JSON-B runtime compatibility provider.

6.2 JSON-B Extension Beans

JSON-B extension types can be registered as Micronaut beans when the application uses the Jsonb bean from micronaut-serde-jsonb.

The default JsonbConfig bean collects the following bean types:

  • JsonbSerializer<T>

  • JsonbDeserializer<T>

  • JsonbAdapter<T, R>

  • PropertyVisibilityStrategy

Use jakarta.annotation.Priority to order multiple serializer, deserializer, or adapter beans. Lower priority values are selected first. If multiple JSON-B callbacks match the same type, the first matching callback in bean order is used.

The following serializer is discovered as a bean:

package example;

import io.micronaut.context.annotation.Requires;
import jakarta.annotation.Priority;
import jakarta.inject.Singleton;
import jakarta.json.bind.serializer.JsonbSerializer;
import jakarta.json.bind.serializer.SerializationContext;
import jakarta.json.stream.JsonGenerator;

@Singleton
@Requires(property = "spec.name", value = "jsonb-extension-beans")
@Priority(10)
public final class ColorSerializer implements JsonbSerializer<Color> {
    @Override
    public void serialize(Color obj, JsonGenerator generator, SerializationContext ctx) {
        generator.write("#" + obj.getValue());
    }
}

A lower-priority serializer for the same type can also exist:

package example;

import io.micronaut.context.annotation.Requires;
import jakarta.annotation.Priority;
import jakarta.inject.Singleton;
import jakarta.json.bind.serializer.JsonbSerializer;
import jakarta.json.bind.serializer.SerializationContext;
import jakarta.json.stream.JsonGenerator;

@Singleton
@Requires(property = "spec.name", value = "jsonb-extension-beans")
@Priority(20)
public final class LowerPriorityColorSerializer implements JsonbSerializer<Color> {
    @Override
    public void serialize(Color obj, JsonGenerator generator, SerializationContext ctx) {
        generator.write("fallback-" + obj.getValue());
    }
}

The lower @Priority value on ColorSerializer means it is registered first and wins for Color.

Deserializers and adapters are registered the same way:

package example;

import io.micronaut.context.annotation.Requires;
import jakarta.annotation.Priority;
import jakarta.inject.Singleton;
import jakarta.json.bind.serializer.DeserializationContext;
import jakarta.json.bind.serializer.JsonbDeserializer;
import jakarta.json.stream.JsonParser;

import java.lang.reflect.Type;

@Singleton
@Requires(property = "spec.name", value = "jsonb-extension-beans")
@Priority(10)
public final class ColorDeserializer implements JsonbDeserializer<Color> {
    @Override
    public Color deserialize(JsonParser parser, DeserializationContext ctx, Type rtType) {
        while (parser.hasNext()) {
            if (parser.next() == JsonParser.Event.VALUE_STRING) {
                return new Color(parser.getString().substring(1));
            }
        }
        throw new IllegalStateException("Expected a JSON string");
    }
}
package example;

import io.micronaut.context.annotation.Requires;
import jakarta.annotation.Priority;
import jakarta.inject.Singleton;
import jakarta.json.bind.adapter.JsonbAdapter;

@Singleton
@Requires(property = "spec.name", value = "jsonb-extension-beans")
@Priority(10)
public final class MilesAdapter implements JsonbAdapter<Miles, String> {
    @Override
    public String adaptToJson(Miles obj) {
        return obj.getValue() + " mi";
    }

    @Override
    public Miles adaptFromJson(String obj) {
        return new Miles(Integer.parseInt(obj.replace(" mi", "")));
    }
}

PropertyVisibilityStrategy is a single JSON-B configuration value. If you expose one as a bean, Micronaut’s normal single-bean selection rules apply.

6.3 Programmatic JSON-B Configuration

Programmatic JSON-B configuration is supported by defining a JsonbConfig bean.

package example;

import io.micronaut.context.annotation.Factory;
import io.micronaut.context.annotation.Requires;
import jakarta.inject.Singleton;
import jakarta.json.bind.JsonbConfig;

@Factory
@Requires(property = "spec.name", value = "jsonb-programmatic-config")
public final class ProgrammaticJsonbConfigFactory {
    @Singleton
    JsonbConfig jsonbConfig() {
        return new JsonbConfig()
            .withSerializers(new ProgrammaticCodeSerializer())
            .withDeserializers(new ProgrammaticCodeDeserializer());
    }
}

The configured serializers, deserializers, and adapters are applied in the order passed to JsonbConfig. If more than one callback matches a type, the first matching configured callback wins.

When you provide a custom JsonbConfig bean, it replaces the default Micronaut-provided config that collects extension beans. Register every JSON-B runtime customization needed by the application on that config.

The serializer and deserializer used by the programmatic config are ordinary JSON-B callback implementations:

package example;

import jakarta.json.bind.serializer.JsonbSerializer;
import jakarta.json.bind.serializer.SerializationContext;
import jakarta.json.stream.JsonGenerator;

public final class ProgrammaticCodeSerializer implements JsonbSerializer<ProgrammaticCode> {
    @Override
    public void serialize(ProgrammaticCode obj, JsonGenerator generator, SerializationContext ctx) {
        generator.write("code:" + obj.getValue());
    }
}
package example;

import jakarta.json.bind.serializer.DeserializationContext;
import jakarta.json.bind.serializer.JsonbDeserializer;
import jakarta.json.stream.JsonParser;

import java.lang.reflect.Type;

public final class ProgrammaticCodeDeserializer implements JsonbDeserializer<ProgrammaticCode> {
    @Override
    public ProgrammaticCode deserialize(JsonParser parser, DeserializationContext ctx, Type rtType) {
        while (parser.hasNext()) {
            if (parser.next() == JsonParser.Event.VALUE_STRING) {
                return new ProgrammaticCode(parser.getString().substring(5));
            }
        }
        throw new IllegalStateException("Expected a JSON string");
    }
}

7 BSON Annotations

The complete set of BSON annotations is supported.

Note that with BSON you can encode both the JSON and to BSON Binary by injecting one of BsonBinaryMapper (Binary) or BsonJsonMapper (JSON).

8 Type Coercion

When a value in the document does not have the shape of the property it is read into, the decoders coerce it. A JSON string is read into an int property, a number is read into a String property, and a floating point number is truncated into an integer property:

{"id": "1234", "name": 42, "count": 9.75}
@Serdeable
record Item(int id, String name, int count) {} // id = 1234, name = "42", count = 9

This is the historical behaviour and remains the default, so that existing applications are unaffected. Applications that would rather reject input of the wrong shape, for example a public API that should answer 400 instead of silently truncating a value, can restrict which coercions are allowed.

Rejecting All Coercions

Set the coercion mode to STRICT to require every value to have the shape of the property it is read into:

micronaut.serde.deserialization.coercion-mode=STRICT

With the configuration above, all three properties in the document at the top are rejected with an InvalidFormatException naming the offending value, while {"id": 1234, "name": "42", "count": 9} still reads.

Selecting Individual Coercions

Each coercion can also be turned on or off on its own. An individual setting always wins over the mode, so a strict application can allow just the coercions it needs:

micronaut.serde.deserialization.coercion-mode=STRICT
micronaut.serde.deserialization.accept-string-as-number=true

The available settings, all under micronaut.serde.deserialization and all enabled by default:

Property Rejects when disabled

accept-float-as-int

9.75 read into int, long, short, byte, char or BigInteger

accept-string-as-number

"42" read into any numeric type

accept-boolean-as-number

true read into any numeric type, as 1

accept-number-as-boolean

1 read into boolean, as any non-zero value

accept-string-as-boolean

"true" read into boolean

accept-scalar-as-string

42 or true read into String

unwrap-single-value-arrays

[42] read into a single int

Note
unwrap-single-value-arrays is the opposite direction of accept-single-value-as-array, which reads a single value into a collection property.

Scope

The setting applies to every format: JSON through Jackson, CBOR, JSON-P, JSON-B, BSON and Oracle JDBC JSON all read it from the same configuration, and it holds however a value reaches a property, including values that are buffered first because a polymorphic discriminator or a creator argument appears later in the document.

XML is not affected, because every scalar in an XML document is textual and there is no shape to coerce from.

A custom Decoder receives the resolved CoercionPolicy the same way it receives the stream limits, and should consult it wherever it reads a value whose shape does not match the requested type:

if (!coercionPolicy.isAllowed(CoercionPolicy.Coercion.STRING_AS_NUMBER)) {
    throw createDeserializationException(CoercionPolicy.Coercion.STRING_AS_NUMBER.message(), text);
}

For decoders that read many values, CoercionPolicy precalculates the set of shapes each target type accepts, so a check costs a single mask test:

// once, when the decoder is created
this.integerShapes = coercionPolicy.allowedShapes(CoercionPolicy.Target.INTEGER);
// per value
if ((integerShapes & shape.bit()) == 0) {
    throw ...;
}

A decoder that creates another decoder over the same data, in particular in decodeBuffer(), must pass its policy on, so that a document is accepted or rejected regardless of which decoder ends up reading a given value.

9 Builders

Micronaut Serialization can deserialize immutable types through a builder instead of a constructor or setters. A builder is used when the type declares one with @Introspected(builder = …​), or with the jackson-databind @JsonDeserialize(builder = …​) annotation which is mapped to the same build-time metadata.

Builders are treated as strict: every property of the builder participates in the same required and default value rules as a property that is populated through a constructor parameter or a setter. Before the build method is called, the properties that are missing from the input are finalized:

  • a property declared as required with @JsonProperty(required = true) fails the deserialization when it is absent from the input, or when it is present but null,

  • a property declaring a default value with @JsonProperty(defaultValue = "…​") (or @Bindable(defaultValue = "…​")) initializes the builder with that value,

  • every other property is left untouched so the defaults of the builder itself are preserved.

The following builder-backed type declares one required property and one property with a default value:

Deserialization then rejects input that omits service, and applies the declared default value of owner:

package example;

import io.micronaut.core.type.Argument;
import io.micronaut.serde.ObjectMapper;
import io.micronaut.serde.exceptions.SerdeException;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Test;

import java.io.IOException;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

@MicronautTest
class StrictBuilderExampleTest {

    @Test
    void testDefaultValueIsAppliedForAMissingProperty(ObjectMapper objectMapper) throws IOException {
        ReleaseRequest request = objectMapper.readValue(
            "{\"service\":\"checkout\"}",
            Argument.of(ReleaseRequest.class)
        );

        assertEquals("checkout", request.getService());
        assertEquals("platform", request.getOwner());
        assertNull(request.getNotes());
    }

    @Test
    void testMissingRequiredPropertyIsRejected(ObjectMapper objectMapper) {
        SerdeException e = assertThrows(SerdeException.class, () -> objectMapper.readValue(
            "{\"owner\":\"growth\"}",
            Argument.of(ReleaseRequest.class)
        ));

        assertTrue(e.getMessage().contains("Required property"));
    }
}
Note
A property that is absent from the input and has neither a required nor a default value declaration never calls the builder, which keeps the values a builder assigns itself, such as a Lombok @Builder.Default field.

10 Custom Serializers & Deserializers

Custom serializers and deserializers for types can be written by implementing the Serializer and Deserializer interfaces respectively and defining beans capable of handling a particular type.

For example given the following class:

package example;

public final class Point {
    private final int x, y;

    private Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int[] coords() {
        return new int[] { x, y };
    }

    public static Point valueOf(int x, int y) {
        return new Point(x, y);
    }
}

A custom serde (a combined serializer and deserializer) can be implemented as follows:

You can now serialize and deserialize classes of type Point:

package example;

import io.micronaut.serde.ObjectMapper;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.io.IOException;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

@MicronautTest
public class PointTest {

    @Test
    void testWriteReadPoint(ObjectMapper objectMapper) throws IOException {
        String result = objectMapper.writeValueAsString(
                Point.valueOf(50, 100)
        );
        Point point = objectMapper.readValue(result, Point.class);
        assertNotNull(point);
        int[] coords = point.coords();
        assertEquals(50, coords[0]);
        assertEquals(100, coords[1]);
    }
}

Serializer Selection

Note that if multiple Serializer beans exist you will get a NonUniqueBeanException, in this case you have a number of options:

  1. Add @Primary to your serializer so it is picked

  2. Add @Order with a higher priority value so it is picked

Deserializer Selection

It is quite common during deserialization to have multiple possible deserializer options. For example a HashSet can be deserialized to both a Collection and a Set.

In these cases you should declare an @Order annotation higher priority value to control which deserializer is chosen by default.

Property Level Serializer or Deserializer

You can also customize the serializer and/or deserializer on a per field, constructor, method etc. basis by using the @Serializable(using=..) and/or @Deserializable(using=..) annotations.

Note
Frequently in this case you will more than one serializer/deserializer for a given type and you should use @Primary or @Secondary to customize bean property so one is selected by default.

For example say you add another secondary Serde to store the previous Point example in reverse order:

You can then define annotations at field, parameter, method etc. level to customize serialization/deserialization for just that case:

11 Enabling Serialization of External Classes

Unlike Jackson, Micronaut Serialization doesn’t allow the arbitrary serialization of any type. As mentioned in the previous section on Custom Serializers, one option to serializing external types is to define a custom serializer, however it is also possible to import types during compilation using the @SerdeImport annotation.

For example consider the following type:

package example;

public class Product {
    private final String name;
    private final int quantity;

    public Product(String name, int quantity) {
        this.name = name;
        this.quantity = quantity;
    }

    public String getName() {
        return name;
    }

    public int getQuantity() {
        return quantity;
    }
}

There are no serialization annotations present on this type and an attempt to serialize this type will result in an error.

To resolve this you can add @SerdeImport to a central location in your project (typically the Application class):

@SerdeImport(Product.class)

Note that if you wish to apply customizations the imported class then you can additionally supply a mixin class. For example:

package example;

import com.fasterxml.jackson.annotation.JsonProperty;

public interface ProductMixin {
    @JsonProperty("p_name")
    String getName();

    @JsonProperty("p_quantity")
    int getQuantity();
}

Then the mixin can be used when declaring SerdeImport:

12 Custom Key Converters

Keys with JSON are always written as Strings however you can use types other than strings when serializing and deserializing Map instances, however you may be required to register a custom TypeConverter.

For example given the following class:

package example;

import io.micronaut.serde.annotation.Serdeable;
import java.util.Map;

@Serdeable
public class Location {
    private final Map<Feature, Point> features;

    public Location(Map<Feature, Point> features) {
        this.features = features;
    }

    public Map<Feature, Point> getFeatures() {
        return features;
    }
}

That defines a custom Feature type for keys. Micronaut Serialization won’t know how to deserialize this type, so along with the type a TypeConverter should be defined:

13 Breaking Changes

This section documents breaking changes between Micronaut Serialization versions:

Micronaut Serialization 3.2.0

Deserialization

  • Which coercions the decoders perform is now configurable, see [coercion]. Every coercion stays enabled by default, so the shapes accepted for a given property are unchanged.

  • A char is read the same way whatever the format and whether or not the value was buffered. A single character string is its natural shape and is accepted in every mode; a number is its code point; and a floating point number is truncated to a code point rather than yielding the first character of its text, which is what the Jackson decoder used to do for 9.75 and true.

  • A property that is buffered before it is read, because a polymorphic discriminator or a creator argument appears later in the document, is now coerced exactly like a property that is read inline. Previously the buffered path rejected a string read into a numeric property, and a number read into a String property, while the inline path accepted them, so whether a document was accepted depended on the order of its properties.

Micronaut Serialization 3.0.0

Deserialization

Micronaut Serialization 3.0 aligns these deserialization defaults with the most commonly used Jackson Databind behavior to make migrations between Jackson Databind and Micronaut Serialization more predictable.

  • The default value of micronaut.serde.deserialization.subtypes-require-default-impl changed from false to true. When a polymorphic deserialization target cannot be resolved to a subtype, Micronaut Serialization now requires a configured default implementation instead of falling back to the supertype by default.

  • Explicit null values in input are no longer skipped for bean properties that are not annotated as nullable. For reference properties this means an explicit null is applied to the property; missing properties continue to use the existing default-value handling.

  • Explicit null values for primitive properties or explicitly non-null properties now fail deserialization by default. For primitive properties, this matches Jackson Databind’s FAIL_ON_NULL_FOR_PRIMITIVES default. Set micronaut.serde.deserialization.fail-on-null-for-primitives=false to deserialize explicit null primitive values as the Java primitive default value instead.

  • The io.micronaut.serde.Deserializer.deserialize(Decoder, DecoderContext, Argument<? super T>) method now returns a non-null value. Callers that accept nullable input values should call deserializeNullable(Decoder, DecoderContext, Argument<? super T>) instead, and deserializers that support nullable values should override deserializeNullable or implement io.micronaut.serde.util.NullableDeserializer.

Deprecations

  • The following constructors of io.micronaut.serde.bson.BsonJsonMapper deprecated previously have been removed. Use BsonJsonMapper(SerdeRegistry, SerdeConfiguration) instead.

    • BsonJsonMapper(SerdeRegistry)

    • BsonJsonMapper(SerdeRegistry, Class<?>)

  • The class io.micronaut.serde.support.serdes.CoreSerdes has been removed. It wasn’t deprecated explicitly, but all it’s members were, and it is no longer used.

  • The interface method io.micronaut.serde.Deserializer.allowNull() deprecated previously was removed. Use the default or override deserializeNullable(Decoder, DecoderContext, Argument<? super T>) instead

  • The method io.micronaut.serde.util.CustomizableDeserializer.allowNull() was removed. This method was deprecated and removed in the super interface Deserializer. It previously raised an IllegalStateException if invoked.

  • All the static fields of io.micronaut.serde.support.DefaultSerdeRegistry deprecated previously were removed. These were aliases for constants defined in the internal class io.micronaut.serde.support.serdes.Serdes and shouldn’t be exposed otherwise.

  • The following Singleton constructors of DefaultSerdeRegistry deprecated previously were removed. The remaining constructor DefaultSerdeRegistry(BeanContext, SerdeIntrospections, ConversionService, SerdeConfiguration, SerializationConfiguration, DeserializationConfiguration) is used instead.

    • DefaultSerdeRegistry(BeanContext, ObjectSerializer, ObjectDeserializer, Serde<Object[]>, SerdeIntrospections, ConversionService, SerdeConfiguration, SerializationConfiguration, DeserializationConfiguration)

    • DefaultSerdeRegistry(BeanContext, ObjectSerializer, ObjectDeserializer, Serde<Object[]>, SerdeIntrospections, ConversionService)

  • The following Singleton constructors of io.micronaut.serde.json.stream.JsonStreamMapper deprecated previously were removed. The remaining constructor JsonStreamMapper(SerdeRegistry, SerdeConfiguration) is used instead.

    • JsonStreamMapper(SerdeRegistry)

    • JsonStreamMapper(SerdeRegistry, Class<?>)

  • The internal class constructor io.micronaut.serde.support.deserializers.ObjectDeserializer(SerdeIntrospections, DeserializationConfiguration, SerdeDeserializationPreInstantiateCallback) deprecated previously has been removed. ObjectDeserializer(SerdeIntrospections, DeserializationConfiguration, SerdeConfiguration, SerdeDeserializationPreInstantiateCallback) is used instead.

  • The internal class constructor io.micronaut.serde.support.serializers.ObjectSerializer(SerdeIntrospections, BeanContext) deprecated previously has been removed. ObjectSerializer(SerdeIntrospections, SerdeConfiguration, SerializationConfiguration, BeanContext) is used instead.

  • The following constructors of io.micronaut.serde.oracle.jdbc.json.OracleJdbcJsonBinaryObjectMapper deprecated previously have been removed. The internal constructor OracleJdbcJsonBinaryObjectMapper(SerdeRegistry registry, SerdeConfiguration) is used instead.

    • OracleJdbcJsonBinaryObjectMapper(SerdeRegistry)

    • OracleJdbcJsonBinaryObjectMapper(SerdeRegistry, Class<?>)

  • The following constructors of io.micronaut.serde.oracle.jdbc.json.OracleJdbcJsonTextObjectMapper deprecated previously have been removed. The internal constructor OracleJdbcJsonTextObjectMapper(SerdeRegistry registry, SerdeConfiguration) is used instead.

    • OracleJdbcJsonTextObjectMapper(SerdeRegistry)

    • OracleJdbcJsonTextObjectMapper(SerdeRegistry, Class<?>)

  • The Singleton constructor io.micronaut.serde.oracle.jdbc.json.serde.OracleJsonBinarySerde() deprecated previously have been removed. OracleJsonBinarySerde(Serde<byte[]>) is used instead.

14 Repository

You can find the source code of this project in this repository: