On this page
Serialization
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 or BSON.
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 :micronaut-benchmarks:jmh run on GraalVM Java 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 |
394,478 ops/s |
391,556 ops/s |
527,497 ops/s |
409,800 ops/s |
Deserialize average time |
3,282 ns/op |
3,165 ns/op |
3,026 ns/op |
3,120 ns/op |
Round-trip average time |
6,399 ns/op |
6,117 ns/op |
4,865 ns/op |
5,165 ns/op |
In this run, generated Micronaut Serialization had the highest serialization throughput, about 33.7% faster than Jackson Databind and about 28.7% faster than the runtime fallback. It was also the fastest deserialization path, about 7.8% faster than Jackson Databind and about 3.0% faster than the runtime fallback, and the fastest combined serialize-and-deserialize round trip, about 24.0% faster than Jackson Databind and about 5.8% 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:
-
Declare the @Serdeable annotation at the type level in your source code to allow the type to be serialized or deserialized.
-
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.
-
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.
For this project, you can find a list of releases (with release notes) here:
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.
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());
}
}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-jsonpis the existing JSON-P stream integration backed by Eclipse Parsson. -
micronaut-serde-jsonp-implis the Micronaut-native JSON-P provider. It implementsjakarta.json.spi.JsonProviderwithout depending on Parsson and does not use reflection. -
micronaut-serde-jsonbis the Micronaut Serialization backed JSON-B runtime provider. It implementsjakarta.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());
}
}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());
}
}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.
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=81By 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=DOTTEDWith 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.
|
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 |
|---|---|---|
✅ |
||
❌ |
||
✅ |
unsupported members: |
|
✅ |
unsupported members: |
|
❌ |
||
✅ |
||
✅ |
||
✅ |
||
✅ |
supported for enum properties using |
|
✅ |
supported only on types, implement the io.micronaut.serde.PropertyFilter interface |
|
✅ |
||
✅ |
||
❌ |
||
❌ |
||
✅ |
unsupported members: |
|
✅ |
||
✅ |
||
✅ |
unsupported members: |
|
✅ |
||
✅ |
||
✅ |
Supported for explicit property-level merge during |
|
✅ |
||
✅ |
||
✅ |
||
❌ |
Not supported for security reasons |
|
✅ |
||
✅ |
unsupported members: |
|
✅ |
||
✅ |
||
✅ |
Only |
|
✅ |
||
✅ |
unsupported members: |
|
✅ |
unsupported members: |
|
✅ |
@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 |
|---|---|
Only with the built-in naming strategies |
|
Only the |
|
Only the |
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 scopecompileOnly("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.
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);
}
}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.
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 |
|---|---|---|
Yes |
||
Yes |
||
Yes |
||
Yes |
||
Yes |
||
Yes |
||
Yes |
||
Yes |
Supported by the JSON-B runtime compatibility provider. |
|
Yes |
Supported by the JSON-B runtime compatibility provider. |
|
Yes |
Subtype aliases are read from |
|
Yes |
Used with |
|
Yes |
Supported by the JSON-B runtime compatibility provider. |
|
Yes |
Supported by the JSON-B runtime compatibility provider. |
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.
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");
}
}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).
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:
-
Add
@Primaryto your serializer so it is picked -
Add
@Orderwith 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:
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:
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:
This section documents breaking changes between Micronaut Serialization versions:
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-implchanged fromfalsetotrue. 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
nullvalues in input are no longer skipped for bean properties that are not annotated as nullable. For reference properties this means an explicitnullis applied to the property; missing properties continue to use the existing default-value handling. -
Explicit
nullvalues for primitive properties or explicitly non-null properties now fail deserialization by default. For primitive properties, this matches Jackson Databind’sFAIL_ON_NULL_FOR_PRIMITIVESdefault. Setmicronaut.serde.deserialization.fail-on-null-for-primitives=falseto deserialize explicitnullprimitive 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 calldeserializeNullable(Decoder, DecoderContext, Argument<? super T>)instead, and deserializers that support nullable values should overridedeserializeNullableor implementio.micronaut.serde.util.NullableDeserializer.
Deprecations
-
The following constructors of
io.micronaut.serde.bson.BsonJsonMapperdeprecated previously have been removed. UseBsonJsonMapper(SerdeRegistry, SerdeConfiguration)instead.-
BsonJsonMapper(SerdeRegistry) -
BsonJsonMapper(SerdeRegistry, Class<?>)
-
-
The class
io.micronaut.serde.support.serdes.CoreSerdeshas 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 overridedeserializeNullable(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 interfaceDeserializer. It previously raised anIllegalStateExceptionif invoked. -
All the static fields of
io.micronaut.serde.support.DefaultSerdeRegistrydeprecated previously were removed. These were aliases for constants defined in the internal classio.micronaut.serde.support.serdes.Serdesand shouldn’t be exposed otherwise. -
The following Singleton constructors of
DefaultSerdeRegistrydeprecated previously were removed. The remaining constructorDefaultSerdeRegistry(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.JsonStreamMapperdeprecated previously were removed. The remaining constructorJsonStreamMapper(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.OracleJdbcJsonBinaryObjectMapperdeprecated previously have been removed. The internal constructorOracleJdbcJsonBinaryObjectMapper(SerdeRegistry registry, SerdeConfiguration)is used instead.-
OracleJdbcJsonBinaryObjectMapper(SerdeRegistry) -
OracleJdbcJsonBinaryObjectMapper(SerdeRegistry, Class<?>)
-
-
The following constructors of
io.micronaut.serde.oracle.jdbc.json.OracleJdbcJsonTextObjectMapperdeprecated previously have been removed. The internal constructorOracleJdbcJsonTextObjectMapper(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.
You can find the source code of this project in this repository: