References
Serialization
How Infinitic uses serialization
Serialization is fundamental to the distributed nature of tasks and workflows in Infinitic. It enables data transfer between different components of the system. Let's examine how serialization is used in different scenarios.
Consider a method myMethod in a Service or Workflow interface:
Bar myMethod(Foo foo);
fun myMethod(foo: Foo): Bar;
In Workflows
When myMethod is part of a Workflow interface and an Infinitic Client synchronously dispatches this method:
The Client serializes the
Fooargument to JSON, and sent it in a command to a Workflow worker.The Workflow worker receiving the command, deserializes the JSON to a
Fooobject and executesmyMethodusing this deserialized object.Once the method completed, the Workflow worker serializes the
Barresult to JSON, and send it in an event back to the initial Client.When receiving the event, the Client deserializes the JSON to a
Barobject, and uses it.
In Services
When myMethod is part of a Service interface and an Infinitic Workflow worker synchronously dispatches this method as a task:
The Workflow worker serializes the
Fooargument to JSON, and send it in a command to a Service worker.The Service worker receiving the event deserializes the JSON to a
Fooobject and executesmyMethodusing this deserialized object.Once the method completed, the Service worker serializes the
Barresult to JSON, and send it in an event to a Workflow worker.The Workflow worker receiving the event deserializes the JSON to a
Barobject, and continues the workflow execution.
Key Points
- Serialization occurs at every transition between system components (client, workflow worker, service worker).
- JSON is used as the serialization format for data transfer.
- Both method arguments (
Foo) and return values (Bar) must be serializable. - Proper serialization/deserialization ensures data integrity across the distributed system.
- While serialization enables distributed processing, it can impact performance for large data structures or high-frequency operations
Java Serialization
For Java, Infinitic uses FasterXML/jackson to serialize/deserialize into/from JSON.
Serialization Testing
To verify if Foo is properly serializable, use the following test pattern with foo being an object of type Foo (the same applies for Bar) :
import io.infinitic.serDe.java.Json;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.ObjectReader;
...
// Get Foo Type object
Type fooType = Foo.class;
// get writer for Foo
ObjectWriter objectWriter = Json.getMapper().writerFor(fooType);
// string representation of the foo object
String json = objectMapper.writeValueAsString(foo);
// get reader for Foo
ObjectReader objectReader = Json.getMapper().readerFor(fooType);
// deserialize json to Foo
String foo2 = objectReader.readValue(json);
// checking everything is ok
assert foo2.equals(foo);
Handling Different Types
Concrete Classes: The above pattern works directly for concrete classes.
Hierarchical Classes: If
Foois an open class, that only have 2 subclassesFooAandFooB, add Jackson annotations to provide type information:@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "klass", visible = true) @JsonSubTypes({ @JsonSubTypes.Type(value = FooA.class, name = "FooA"), @JsonSubTypes.Type(value = FooB.class, name = "FooB") }) abstract class Foo {Interfaces: If
Foois an interface, only implemented by classesFooAandFooB, add Jackson annotations to provide type information:@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "klass") @JsonSubTypes({ @JsonSubTypes.Type(value = FooA.class, name = "FooA"), @JsonSubTypes.Type(value = FooB.class, name = "FooB") }) interface Foo {Complex Types:
If
Foois a complex type (e.g., collections, arrays, maps), it should be serializable if all its components are serializable. Note that the test above must be updated to extract the generic type from the method signature:Method method = klass.getMethod("myMethod", Foo.class); Type fooType = Arrays.stream(method).getGenericParameterTypes()).findFirst().orElseThrow();If
Baris a complex type (e.g., collections, arrays, maps), it should be serializable if all its components are serializable. Note that the test above must be updated to extract the generic type from the method signature:Method method = klass.getMethod("myMethod", Foo.class); Type barType = method.getGenericReturnType();
Custom Object Mapper
Infinitic allows you to customize the Jackson ObjectMapper used for serialization and deserialization. This feature is useful when you need to adjust serialization behavior to match your specific requirements. Here's how to implement a custom ObjectMapper:
import io.infinitic.serDe.java.Json;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.MapperFeature;
// Retrieve the current ObjectMapper
ObjectMapper customMapper = Json.getMapper();
// Configure the ObjectMapper as needed
customMapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true);
// Add more configurations as required
// customMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
// customMapper.registerModule(new JavaTimeModule());
// Set the customized ObjectMapper for Infinitic to use
Json.setMapper(customMapper);
// Now, initialize your Infinitic client or worker
Key points:
- Always start with Json.getMapper() to ensure you're building upon Infinitic's base configuration.
- Apply your custom configurations to this ObjectMapper instance.
- Set the customized ObjectMapper using Json.setMapper() before initializing Infinitic clients or workers.
- This customization affects all subsequent serialization/deserialization within Infinitic.
- Ensure it is used consistently in both clients and workers
- Ensure it is used consistently with existing messages
JsonView Support
Since version 0.15.0, Infinitic supports Jackson's @JsonView annotation, which helps refine object serialization.
Service Interface
In Infinitic, interfaces serve as contracts for remote services:
import com.fasterxml.jackson.annotation.JsonView;
public interface UserService {
@JsonView(View.R1.class)
User getUser(@JsonView(View.P1.class) Request request);
}
- the
requestobject (parameter of the RPC call) is serialized using theView.P1.classJackson view. - the
userobject (response of the RPC call) is deserialized using theView.R1.classview.
Service Implementation
In Service workers where the actual implementation runs:
import com.fasterxml.jackson.annotation.JsonView;
public class UserServiceImpl implements UserService {
@Override
@JsonView(View.R2.class)
public String getUser(@JsonView(View.P2.class) Request request) {
...
return user
}
}
- the
requestobject (parameter of the RPC call) is deserialized using theView.P2.classJackson view. - the
userobject (response of the RPC call) is serialized using theView.R2.classview.
Often, you won't need @JsonView annotations in the Service implementation, as Infinitic will use the same views defined in the interface. If you do not want to use any view, simply add an empty @JsonView annotation.
Workflow Interface
Interfaces also serve as contracts for remote workflows:
import com.fasterxml.jackson.annotation.JsonView;
public interface UserWorkflow {
@JsonView(View.R1.class)
Status welcome(@JsonView(View.P1.class) Registration registration);
}
- the
registrationobject (parameter of the RPC call) is serialized using theView.P1.classJackson view. - the
statusobject (response of the RPC call) is deserialized using theView.R1.classview.
Workflow Implementation
In Workflow workers where the actual implementation runs:
import io.infinitic.annotations.Name;
import com.fasterxml.jackson.annotation.JsonView;
import hello.world.services.HelloService;
import io.infinitic.workflows.Workflow;
public class UserWorkflowImpl extends Workflow implements UserWorkflow {
@Override
@JsonView(View.R2.class)
Status welcome(@JsonView(View.P2.class) Registration registration) {
...
return status;
}
}
- the
registrationobject (parameter of the RPC call) is deserialized using theView.P2.classJackson view. - the
statusobject (response of the RPC call) is serialized using theView.R2.classview.
Often, you won't need @JsonView annotations in the Workflow implementation, as Infinitic will use the same views defined in the interface. If you do not want to use any view, simply add an empty @JsonView annotation.
Kotlin Serialization
For Kotlin, we recommend using kotlinx-serialization, a native serialization library developed by JetBrains.
To use kotlinx.serialization in your Infinitic project:
- Add the
kotlinx.serializationplugin to your build script. - Apply the
@Serializableannotation to your data classes.
See the Kotlin documentation for more details.
If kotlinx.serialization is not used (i.e., your classes don't have a built-in Kotlin serializer), Infinitic will fall back to FasterXML/jackson for serialization/deserialization, as it does for Java. In this case, the guidelines below do not apply.
Serialization Testing
To verify if Foo is properly serializable, use the following test pattern with foo being an instance of Foo (the same applies for Bar) :
import kotlinx.serialization.serializer
import kotlinx.serialization.json.Json
import kotlin.reflect.typeOf
...
// Get Json Kotlin serializer
val json = Json
// Get Foo serializer
val serializer = serializer(typeOf<Foo>())
// string representation of the foo object
val jsonStr = json.encodeToString(serializer, foo)
// deserialize json to Foo
val foo2 = json.decodeFromString(serializer, jsonStr)
// checking everything is ok
require(foo2 == foo)
Handling Different Types
Concrete Classes: The easiest way is to use data classes with a
kotlinx.serialization.Serializableannotation. For example :import kotlinx.serialization.Serializable @Serializable data class Foo(Hierarchical Classes: If
Foois an open class, the easiest way to use asealed class:import kotlinx.serialization.Serializable @Serializable sealed class Foo(If
Foois not sealed, you need to provide the polymorphic info to the serializer.For example if
Foois not sealed and only has 2 subclassesFooAandFooB:import io.infinitic.serDe.kotlin.json import kotlinx.serialization.json.Json import kotlinx.serialization.modules.SerializersModule ... // set a custom Json serializer json = Json { serializersModule = SerializersModule { polymorphic(Foo::class, FooA::class, FooA.serializer()) polymorphic(Foo::class, FooB::class, FooB.serializer()) } }Interfaces: If
Foois an interface, the easiest way to use asealed interface:import kotlinx.serialization.Serializable @Serializable sealed interface Foo {If
Foois not sealed, you need to provide the polymorphic info to the serializer.For example, if
Foois not sealed and only implemented by classesFooAandFooB:import io.infinitic.serDe.kotlin.json import kotlinx.serialization.json.Json import kotlinx.serialization.modules.SerializersModule ... // set a custom Json serializer json = Json { serializersModule = SerializersModule { polymorphic(Foo::class, FooA::class, FooA.serializer()) polymorphic(Foo::class, FooB::class, FooB.serializer()) } }Complex Types: If
Foois a complex type (e.g., collections, arrays, maps), it should be serializable as soon as its components are serializable using Kotlin serializer.
Custom Json Serializer
Infinitic allows you to customize the io.infinitic.serDe.kotlin.json object used for serialization and deserialization. This feature is useful when you need to adjust serialization behavior to match your specific requirements, for example to add polymorphic info as described above. Here's how to implement a custom one:
import io.infinitic.serDe.kotlin.json
import kotlinx.serialization.json.Json
...
// set a custom Json serializer
json = Json {
classDiscriminator = "#klass"
ignoreUnknownKeys = true
}
// Now, initialize your Infinitic client or worker
Key points:
- Mutate the property
io.infinitic.serDe.kotlin.jsonbefore initializing Infinitic clients or workers. - This customization affects all subsequent serialization/deserialization operations within Infinitic.
- Ensure it is used consistently in both clients and workers
- Ensure it is used consistently with existing messages
