Serialization
Serialization in IPF is done with Jackson, and its own documentation will cover far more ground than this page intends to, and there are innumerable tutorials and online resources that can help with understanding and using Jackson. A good place to start are the official docs.
The goal of this page is a short introduction to serialization within IPF, including the default behaviour and how it can be customised, and some caveats and recommendations.
Concepts
This section is intended to be brief. All the concepts listed here are covered in further detail in later sections.
SerializationHelper
SerializationHelper is the main entry-point for serialization.
It provides convenience methods for serializing to and deserializing from JSON, and ultimately delegates to its underlying singleton Jackson com.fasterxml.jackson.databind.ObjectMapper instance.
The underlying singleton ObjectMapper instance is created using the ObjectMapperFactory.
Examples
//Access the singleton ObjectMapper instance
ObjectMapper objectMapper = SerializationHelper.objectMapper();
//e.g. to convert between types
objectMapper.convertValue(object, SomeOtherType.class);
//Convenience methods to serialize to JSON using the singleton ObjectMapper
String json = SerializationHelper.objectToString(object);
//And to deserialize from JSON
MyObject deserialized = SerializationHelper.stringToObject(json, MyObject.class);
ObjectMapperFactory
com.iconsolutions.ipf.core.shared.api.serializer.ObjectMapperFactory is the main entry point for creating new ObjectMapper instances.
If you need an instance other than the one provided by SerializationHelper.objectMapper(), you would use this class. SerializationHelper uses ObjectMapperFactory to create its own singleton ObjectMapper instance.
Examples
//Create a new ObjectMapper instance that behaves the same as SerializationHelper.objectMapper()
ObjectMapper serializationHelperObjectMapper = ObjectMapperFactory.createObjectMapper();
//Create a new ObjectMapper instance with additional configured customisation
ObjectMapper customSerializationHelperObjectMapper = ObjectMapperFactory.createObjectMapper(config);
//Create a new named ObjectMapper instance that must be configured separately from serialization-helper
ObjectMapper customObjectMapper = ObjectMapperFactory.createObjectMapper("my-object-mapper", config);
Akka Jackson
The ObjectMapper instances are configured using Akka Jackson, which means the serialization behaviour can be customised with Hocon configuration.
Default configuration is included for a serializer called serialization-helper, and this serializer can be customised with additional config.
akka.serialization.jackson.serialization-helper {
//Additional modules can be added to support custom serialization/deserialization behaviour
jackson-modules += "MyCustomModule"
}
New serializers may also be defined, which is a good way to customise serialization without changing the behaviour of serialization-helper.
akka.serialization.jackson.my-serializer = "akka.serialization.jackson.JacksonJsonSerializer"
//Custom serializer can inherit the default serialization-helper behaviour, and provide additional customisation
akka.serialization.jackson.my-serializer = ${akka.serialization.jackson.serialization-helper} {
jackson-modules += "MyCustomModule"
}
Getting Started
Using The Default SerializationHelper
In many cases Default Serialization Behaviour will be enough, which means serialization can be performed using the convenience methods on SerializationHelper, or by accessing the underlying ObjectMapper instance.
String json = SerializationHelper.objectToString(object);
MyObject deserialized = SerializationHelper.stringToObject(json, MyObject.class);
Map<String, Object> b = SerializationHelper.objectMapper().convertValue(deserialized, new TypeReference<Map<String, Object>>() {
});
Customising Default SerializationHelper
There will of course be cases where the default serialization behaviour is not sufficient, e.g. when types must be serialized in a custom way.
Customisation should ideally be done via configuration, See the Configuration Reference for the full set of configuration options.
Avoid programmatically configuring/mutating the default ObjectMapper instance.
Registering Custom Modules
Given the following custom module
public final class MyModule extends SimpleModule {
@Override
public void setupModule(final SetupContext context) {
super.setupModule(context);
final var serializers = new SimpleSerializers();
serializers.addSerializer(SomeType.class, new JsonSerializer<>() {
@Override
public void serialize(final SomeType value, final JsonGenerator gen, final SerializerProvider serializers) {
//Custom serialization behaviour for SomeType
}
});
context.addSerializers(serializers);
final var deserializers = new SimpleDeserializers();
deserializers.addDeserializer(SomeType.class, new JsonDeserializer<>() {
@Override
public SomeType deserialize(final JsonParser p, final DeserializationContext ctxt) {
//Custom deserialization behaviour for SomeType
}
});
context.addDeserializers(deserializers);
}
record SomeType(String name) {
}
}
The module can be added to the serialization-helper serializer with the following config:
akka.serialization.jackson.serialization-helper.jackson-modules += "package.of.my.module.MyModule"
Caveats
Changing the default serialization behaviour should be approached with caution, and is probably best avoided. In most cases, adding custom supported types through modules will be fine, but changing the default serialization behaviour will impact all other code that uses the default serialization. When additional configuration is required, instead consider Creating Custom Serializers.
Programmatic configuration, i.e. mutating the ObjectMapper instance, should be avoided because it relies on that code-path being invoked for the configuration to take effect, e.g. avoid doing the following
SerializationHelper.objectMapper().registerModule(new MyModule())
Using IPF Serialization in a Spring Application
Spring web applications typically configure their own ObjectMapper bean, and Spring comes with its own mechanisms for customisation of the ObjectMapper instance.
You may prefer to use the IPF serialization behaviour. To do so, depend on ipf-serialization-autoconfigure.
<dependency>
<groupId>com.iconsolutions.ipf.core.platform</groupId>
<artifactId>ipf-serialization-autoconfigure</artifactId>
</dependency>
This Spring autoconfiguration module configures itself before any Spring Jackson autoconfiguration, providing the same default ObjectMapper (it’s a different instance) used by SerializationHelper.
Creating Custom Serializers
Creating new serializers, which will be necessary when the default serialization behaviour is not sufficient, can be achieved in a few different ways.
Typically downstream projects create their own ObjectMapper instance, configure it programmatically, and make that instance available in all the places it is needed, but it is also possible to define and configure custom serializers via hocon.
The approach you choose may depend on your requirements, e.g. do you need the default behaviour with small changes, or do you need an entirely new ObjectMapper with none of the IPF defaults.
When the defaults are almost enough, but you need to change how null and empty values are serialized, you could either programmatically create a new default instance, and override the serialization inclusions, e.g.
//Creates a new instance of the default serialization ObjectMapper
var mapper ObjectMapperFactory.createObjectMapper();
//And overrides it to always write nulls and empty objects when serializing to JSON
mapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);
Alternatively, you can define a custom mapper with its own modules for customisation, e.g.
akka {
serializers.my-custom-mapper = "akka.serialization.jackson.JacksonJsonSerializer"
//Inherits serialization-helper, but with customisations
serialization.jackson.my-custom-mapper = ${akka.serialization.jackson.serialization-helper} {
jackson-modules += "com.food.bar.MyCustomModule"
}
}
/**
* Overrides the default serialization inclusion configuration
*/
public final class MyCustomModule extends SimpleModule {
@Override
public void setupModule(final SetupContext context) {
super.setupModule(context);
final ObjectMapper objectMapper = context.getOwner();
objectMapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);
}
}
var mapper = ObjectMapperFactory.createObjectMapper("my-custom-mapper", config);
When a completely new serializer is required, omit the placeholder used to inherit serialization-helper, e.g.
akka {
serializers.my-custom-mapper = "akka.serialization.jackson.JacksonJsonSerializer"
}
| When serialization is to be shared, e.g. you produce a client and server application, consider introducing a shared serialization module with its own config/programmatic setup, and helper classes, rather than duplicating the configuration and initialisation of the serializer. |
Default Serialization Behaviour
The default serialization-helper ObjectMapper instance is a customised instance of what’s defined for Akka Jackson serialization, e.g. configuration under akka.serialization.jackson, which itself is a customised instance of the default ObjectMapper, e.g. new ObjectMapper().
| All features and settings not mentioned here can be assumed to be the Jackson defaults. |
Serialization Features
| Feature | Enabled/Disabled | Description |
|---|---|---|
|
Enabled |
The duration |
|
Enabled |
An exception will be thrown when attempting to serialize an object that does not have any serializable properties. |
Deserialization Features
| Feature | Enabled/Disabled | Description |
|---|---|---|
|
Enabled |
Sets the datatype used when deserializing floating point numbers, in this case |
Modules
The following modules are included in the default serializer.
| Module | Description |
|---|---|
A Jackson module that adds support for Scala types |
|
A Jackson module that adds support for accessing parameter names; a feature added in JDK 8. |
|
A Jackson module that adds support for jdk8 types, e.g. |
|
|
An IPF module that adds support for java time types, e.g. |
|
An IPF module that makes up for an issue with enum types with incomplete |
|
An IPF module that adds support for DOM elements |
|
An IPF module that adds support for deserializing raw |
Configuration Reference
Akka Jackson is used to configure the serializers, so the best place to start is the official docs. For specific configuration options, see the Additional Features section.
The configuration details in the Additional Features section, e.g. everything under akka.serialization.jackson may also be applied to the default serialization-helper akka.serialization.jackson.serialization-helper, and to custom serializers, e.g. akka.serialization.jackson.my-custom-serializer.
Akka Jackson doesn’t support configuring the serialization inclusions for the ObjectMapper instance, and typically this is done programmatically, e.g. new ObjectMapper().setSerializationInclusion(JsonIncludes.NON_NUll), but this can be achieved with a custom module that customises the underlying ObjectMapper instance, e.g.
public final class JsonIncludeOverrideModule extends SimpleModule {
@Override
public void setupModule(final SetupContext context) {
super.setupModule(context);
final ObjectMapper objectMapper = context.getOwner();
objectMapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);
}
}
Do’s, Don’ts, Suggestions, Rants
Supporting Custom Types
Avoid using SerializationHelper, or any other ObjectMapper instance from within custom com.fasterxml.jackson.databind.JsonSerializer and com.fasterxml.jackson.databind.JsonDeserializer implementations, e.g. don’t do
final class SomeOtherObjectJsonSerializer extends JsonSerializer<SomeOtherObject> {
@Override
public void serialize(final SomeObject value, final JsonGenerator gen, final SerializerProvider serializers) throws IOException {
//Use SerializationHelper
final var string = SerializationHelper.objectToString(value);
gen.writeString(string);
}
}
This custom JsonSerializer may be used within any custom ObjectMapper instance, but relies on the default serialization-helper ObjectMapper, which may behave differently at runtime, depending on configuration.
Prefer the mechanism provided to you, e.g. in the above serializer you can probably achieve what you need with the JsonGenerator and SerializerProvider arguments. The JsonDeserializer provides similar facilities.
===
Share Custom Serialization
If you have specific serialization requirements and the default serialization-helper is not sufficient, consider creating your own named serializer, and placing its config and helper classes/methods, and of course, tests, in a new module. This module can then be used by other downstream projects.