Support for kotlin value classes as field types in framework messages

Are there plans to support kotlin value classes for common, domain-specific concept in axon messages - commands, queries and events.

I have attempted using a simple one such as EmailAddress, while using json serialization for all message types only to be met with an AxonConfigurationException

> java.util.concurrent.ExecutionException: org.axonframework.common.AxonConfigurationException:
> SagaEventHandler public void accounts.command.transactions.EmailVerificationTx.handle(apis.accounts.EmailAddressVerified) defines a property emailAddress that is not defined on the Event it declares to handle (apis.accounts.EmailAddressVerified)

This is the event data class…

data class EmailAddressVerified(val accountId: UUID, val emailAddress: EmailAddress)

and the value class definition…

@JvmInline
value class EmailAddress(@field:Email(regexp = EMAIL_REGEXP) val value: String)

Please note that I am using kotlin 1.9.23 and axon-spring-boot-starter:4.9.3 in a spring boot v3.2.5 app.

Thanks.

Are you using jackson as serializer? Then you would have to register the jackson-module-kotlin so the jackson object-mapper correctly deals with data classes …

Hi @Jan_Galinski I must thank you for pointing me in the right direction. Having attempted to register the kotlin module like so…


    @Bean
    @Primary
    fun mapper(): ObjectMapper {
        return jacksonObjectMapper().registerKotlinModule()
    }

    @Bean
    @Primary
    fun jacksonSerializer(mapper: ObjectMapper): JacksonSerializer {
        return JacksonSerializer.builder().objectMapper(mapper).build()
    }

I expected to achieve the expected result until I experienced a compatibility issue with the set of jackson libraries used by the current (4.9.3) version of the axon-messaging framework…

com.fasterxml.jackson.core » jackson-databind (optional)

com.fasterxml.jackson.datatype » jackson-datatype-jsr310 (optional)

The above libraries point to version 2.14.3 whilst the latest supported version currently is 2.17.1 This would be just fine until you realize that support for inline value classes was added in v2.17 as seen here

I attempted overriding axon’s 2.14 libraries with 2.17 variants (jackson-core, jackson-databind and jackson-kotlin-module) only to be met with an error that prevents successful application startup…

Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'serializer': Requested bean is currently in creation: Is there an unresolvable circular reference?

I could use some help here.

Update
The reported exception (above) was due to a misconfiguration in my Axon Configuration class that I have now resolved.

The initial issue (with kotlin inline value classes) still persists even after upgrading to v2.17.1 of jackson-databind, jackson-core, jackson-kotlin-module and jackson-datatype-jsr310

The exception still stands…

Caused by: org.axonframework.common.AxonConfigurationException: SagaEventHandler public void accounts.command.transactions.EmailVerificationTx.handle(apis.accounts.EmailAddressVerified) defines a property emailAddress that is not defined on the Event it declares to handle (apis.accounts.EmailAddressVerified)
	at org.axonframework.modelling.saga.PayloadAssociationResolver.createProperty(PayloadAssociationResolver.java:76) ~[axon-modelling-4.9.3.jar:4.9.3]
	at org.axonframework.modelling.saga.PayloadAssociationResolver.lambda$getProperty$0(PayloadAssociationResolver.java:62) ~[axon-modelling-4.9.3.jar:4.9.3]
	at java.base/java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1710) ~[na:na]
	at org.axonframework.modelling.saga.PayloadAssociationResolver.getProperty(PayloadAssociationResolver.java:61) ~[axon-modelling-4.9.3.jar:4.9.3]
	at org.axonframework.modelling.saga.PayloadAssociationResolver.validate(PayloadAssociationResolver.java:47) ~[axon-modelling-4.9.3.jar:4.9.3]
	at org.axonframework.modelling.saga.SagaMethodMessageHandlerDefinition.doWrapHandler(SagaMethodMessageHandlerDefinition.java:77) ~[axon-modelling-4.9.3.jar:4.9.3]
	at org.axonframework.modelling.saga.SagaMethodMessageHandlerDefinition.wrapHandler(SagaMethodMessageHandlerDefinition.java:61) ~[axon-modelling-4.9.3.jar:4.9.3]
	at org.axonframework.messaging.annotation.MultiHandlerEnhancerDefinition.wrapHandler(MultiHandlerEnhancerDefinition.java:113) ~[axon-messaging-4.9.3.jar:4.9.3]
	at org.axonframework.messaging.annotation.MultiHandlerDefinition.createHandler(MultiHandlerDefinition.java:185) ~[axon-messaging-4.9.3.jar:4.9.3]
	at org.axonframework.messaging.annotation.AnnotatedHandlerInspector.initializeMessageHandlers(AnnotatedHandlerInspector.java:206) ~[axon-messaging-4.9.3.jar:4.9.3]
	at org.axonframework.messaging.annotation.AnnotatedHandlerInspector.initialize(AnnotatedHandlerInspector.java:197) ~[axon-messaging-4.9.3.jar:4.9.3]
	at org.axonframework.messaging.annotation.AnnotatedHandlerInspector.createInspector(AnnotatedHandlerInspector.java:153) ~[axon-messaging-4.9.3.jar:4.9.3]
	at org.axonframework.messaging.annotation.AnnotatedHandlerInspector.inspectType(AnnotatedHandlerInspector.java:138) ~[axon-messaging-4.9.3.jar:4.9.3]
	at org.axonframework.messaging.annotation.AnnotatedHandlerInspector.inspectType(AnnotatedHandlerInspector.java:120) ~[axon-messaging-4.9.3.jar:4.9.3]
	at org.axonframework.modelling.saga.metamodel.AnnotationSagaMetaModelFactory.doCreateModel(AnnotationSagaMetaModelFactory.java:94) ~[axon-modelling-4.9.3.jar:4.9.3]
	at java.base/java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1710) ~[na:na]
	at org.axonframework.modelling.saga.metamodel.AnnotationSagaMetaModelFactory.modelOf(AnnotationSagaMetaModelFactory.java:82) ~[axon-modelling-4.9.3.jar:4.9.3]
	at org.axonframework.modelling.saga.repository.AnnotatedSagaRepository$Builder.inspectSagaModel(AnnotatedSagaRepository.java:400) ~[axon-modelling-4.9.3.jar:4.9.3]
	at org.axonframework.modelling.saga.repository.AnnotatedSagaRepository$Builder.buildSagaModel(AnnotatedSagaRepository.java:385) ~[axon-modelling-4.9.3.jar:4.9.3]
	at org.axonframework.modelling.saga.repository.AnnotatedSagaRepository.<init>(AnnotatedSagaRepository.java:80) ~[axon-modelling-4.9.3.jar:4.9.3]
	at org.axonframework.modelling.saga.repository.AnnotatedSagaRepository$Builder.build(AnnotatedSagaRepository.java:373) ~[axon-modelling-4.9.3.jar:4.9.3]
	at org.axonframework.config.SagaConfigurer$SagaConfigurationImpl.lambda$initialize$1(SagaConfigurer.java:219) ~[axon-configuration-4.9.3.jar:4.9.3]
	at org.axonframework.config.Component.get(Component.java:85) ~[axon-configuration-4.9.3.jar:4.9.3]
	at org.axonframework.config.SagaConfigurer$SagaConfigurationImpl.lambda$initialize$2(SagaConfigurer.java:229) ~[axon-configuration-4.9.3.jar:4.9.3]
	at org.axonframework.config.Component.get(Component.java:85) ~[axon-configuration-4.9.3.jar:4.9.3]
	at org.axonframework.config.SagaConfigurer$SagaConfigurationImpl.manager(SagaConfigurer.java:165) ~[axon-configuration-4.9.3.jar:4.9.3]
	at org.axonframework.config.EventProcessingModule.lambda$null$45(EventProcessingModule.java:375) ~[axon-configuration-4.9.3.jar:4.9.3]
	at org.axonframework.config.EventProcessingModule.lambda$buildEventProcessor$47(EventProcessingModule.java:395) ~[axon-configuration-4.9.3.jar:4.9.3]
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:212) ~[na:na]
	at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1709) ~[na:na]
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:556) ~[na:na]
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:546) ~[na:na]
	at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921) ~[na:na]
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:265) ~[na:na]
	at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:702) ~[na:na]
	at org.axonframework.config.EventProcessingModule.buildEventProcessor(EventProcessingModule.java:396) ~[axon-configuration-4.9.3.jar:4.9.3]
	at org.axonframework.config.EventProcessingModule.lambda$null$28(EventProcessingModule.java:238) ~[axon-configuration-4.9.3.jar:4.9.3]
	at org.axonframework.config.Component.get(Component.java:85) ~[axon-configuration-4.9.3.jar:4.9.3]
	at java.base/java.util.HashMap$Values.forEach(HashMap.java:1073) ~[na:na]
	at org.axonframework.config.EventProcessingModule.initializeProcessors(EventProcessingModule.java:243) ~[axon-configuration-4.9.3.jar:4.9.3]
	at org.axonframework.config.LifecycleOperations.lambda$onStart$0(LifecycleOperations.java:62) ~[axon-configuration-4.9.3.jar:4.9.3]
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:212) ~[na:na]
	at java.base/java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:1024) ~[na:na]
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:556) ~[na:na]
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:546) ~[na:na]
	at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921) ~[na:na]
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:265) ~[na:na]
	at java.base/java.util.stream.ReferencePipeline.reduce(ReferencePipeline.java:677) ~[na:na]
	at org.axonframework.config.DefaultConfigurer.invokeLifecycleHandlers(DefaultConfigurer.java:1058) ~[axon-configuration-4.9.3.jar:4.9.3]

This definetaly points to a business/non axon problem.
You could try to serieliaze and deserialize “manually” in a unit test to verify you classes behave as you expect them.

Hi @Jan_Galinski , apis.accounts.EmailAddressVerified is the same data class I have been referencing throughout our conversation.

I’m at a loss, at the moment, with this issue and will appreciate it if you could point me to a reference project that successfully integrates kotlin’s value classes in its messages while utilizing Jackson as its serializer.

Thanks you for the support so far.

Value class

Yes that makes sense. Value classes are not supported by Jackson. Simple solution: use data class

Complicated solution: search for Jackson and value classes, you could succeed with additional mixins or additional libs

Thanks for the quick response, Jan.

jackson-kotlin-module support for value class is available - jackson-module-kotlin/docs/value-class-support.md at 2.17 · FasterXML/jackson-module-kotlin · GitHub although they note that there’s limited compatibility with data classes when it comes to serialization and deserialization.

I’m not entirely sure what to do with that information, however, tests should prove the practicality of the ‘tweaks’ they suggest.