Issue
I am writing a POC about Axon, SpringBoot and MongoDB using Kotlin, I have configured my serializers to use Jackson
in general, events and messages and everything works as expected (Command, Event and Aggregate).
The problem begins when I am trying to perform a Query by QueryGateway
I have got this error:
java.lang.IllegalArgumentException: Retrieved response [class java.util.ArrayList] is not convertible to a List of the expected response type [class com.mohammali.poc.eventsourcing.models.cardboards.CardBoardDomain]
at org.axonframework.messaging.responsetypes.MultipleInstancesResponseType.convert(MultipleInstancesResponseType.java:113) ~[axon-messaging-4.5.15.jar:4.5.15]
at org.axonframework.messaging.responsetypes.MultipleInstancesResponseType.convert(MultipleInstancesResponseType.java:44) ~[axon-messaging-4.5.15.jar:4.5.15]
at org.axonframework.messaging.responsetypes.ConvertingResponseMessage.getPayload(ConvertingResponseMessage.java:85) ~[axon-messaging-4.5.15.jar:4.5.15]
at org.axonframework.queryhandling.DefaultQueryGateway.lambda$query$1(DefaultQueryGateway.java:87) ~[axon-messaging-4.5.15.jar:4.5.15]
at java.base/java.util.concurrent.CompletableFuture$UniAccept.tryFire(CompletableFuture.java:718) ~[na:na]
at java.base/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:510) ~[na:na]
at java.base/java.util.concurrent.CompletableFuture.complete(CompletableFuture.java:2147) ~[na:na]
at org.axonframework.axonserver.connector.query.AxonServerQueryBus$ResponseProcessingTask.run(AxonServerQueryBus.java:761) ~[axon-server-connector-4.5.15.jar:4.5.15]
I have tracked the issue and I believe the problem is the response type comes always as java.util.ArrayList
without the generic type
here is the implementation:
Query Handler
package com.mohammali.poc.eventsourcing.queryhandler.query
import com.mohammali.poc.eventsourcing.models.cardboards.CardBoardDomain
import com.mohammali.poc.eventsourcing.models.cardboards.QueryFindAllCardBoard
import com.mohammali.poc.eventsourcing.queryhandler.data.MongoCardBoardRepository
import org.axonframework.config.ProcessingGroup
import org.axonframework.queryhandling.QueryHandler
import org.springframework.stereotype.Component
@Component
@ProcessingGroup("cardboard-query")
class CardBoardQueryHandler(
private val repository: MongoCardBoardRepository
) {
@QueryHandler
fun on(query: QueryFindAllCardBoard): List<CardBoardDomain> =
repository.findAll().map {
CardBoardDomain(it.id!!, it.name!!, it.width!!, it.height!!)
}
}
Query Caller or QueryGateway
package com.mohammali.poc.eventsourcing.gateway.usecases
import com.mohammali.poc.eventsourcing.models.cardboards.CardBoardDomain
import com.mohammali.poc.eventsourcing.models.cardboards.QueryFindAllCardBoard
import org.axonframework.extensions.kotlin.queryMany
import org.axonframework.queryhandling.QueryGateway
import org.springframework.stereotype.Service
@Service
class CardBoardUseCase(
private val queryGateway: QueryGateway
) {
fun findAll(): List<CardBoardDomain> =
queryGateway.queryMany<CardBoardDomain, QueryFindAllCardBoard>(QueryFindAllCardBoard()).get()
}
Domain I am using
package com.mohammali.poc.eventsourcing.models.cardboards
import com.mohammali.poc.eventsourcing.models.Id
data class CardBoardDomain(
val id: Id,
val name: String,
val width: Double,
val height: Double
)
Custom Serializer
@Bean
@Qualifier("mainObjectMapper")
@Primary
fun createMapper(): ObjectMapper {
val builder = Jackson2ObjectMapperBuilder()
builder.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
val o = builder.build<ObjectMapper>()
.registerKotlinModule()
.registerModule(
SimpleModule()
.addSerializer(Id::class.javaObjectType, IdJsonSerializer())
.addSerializer(Id::class.javaPrimitiveType, IdJsonSerializer())
.addDeserializer(Id::class.javaObjectType, IdJsonDeserializer())
.addDeserializer(Id::class.javaPrimitiveType, IdJsonDeserializer())
)
return o
}
@Bean
@Primary
fun axonJacksonSerializer(objectMapper: ObjectMapper): Serializer =
JacksonSerializer.builder()
.objectMapper(objectMapper)
.build()
Update
I have tried adding defaultTyping
to JacksonSerializer
suggested by Mitchell Herrijgers in this answer but I get a new error:
com.fasterxml.jackson.databind.exc.MismatchedInputException: Unexpected token (START_OBJECT), expected START_ARRAY: need JSON Array to contain As.WRAPPER_ARRAY type information for class java.lang.Object
at [Source: (byte[])"[{"id":"1015764681047937024","name":"test","width":10.0,"height":10.0}]"; line: 1, column: 2] (through reference chain: java.util.ArrayList[0])
at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:59) ~[jackson-databind-2.13.3.jar:2.13.3]
at com.fasterxml.jackson.databind.DeserializationContext.wrongTokenException(DeserializationContext.java:1939) ~[jackson-databind-2.13.3.jar:2.13.3]
at com.fasterxml.jackson.databind.DeserializationContext.reportWrongTokenException(DeserializationContext.java:1673) ~[jackson-databind-2.13.3.jar:2.13.3]
at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer._locateTypeId(AsArrayTypeDeserializer.java:141) ~[jackson-databind-2.13.3.jar:2.13.3]
at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer._deserialize(AsArrayTypeDeserializer.java:96) ~[jackson-databind-2.13.3.jar:2.13.3]
at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer.deserializeTypedFromAny(AsArrayTypeDeserializer.java:71) ~[jackson-databind-2.13.3.jar:2.13.3]
at com.fasterxml.jackson.databind.deser.std.UntypedObjectDeserializer$Vanilla.deserializeWithType(UntypedObjectDeserializer.java:781) ~[jackson-databind-2.13.3.jar:2.13.3]
at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer._deserializeFromArray(CollectionDeserializer.java:357) ~[jackson-databind-2.13.3.jar:2.13.3]
at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:244) ~[jackson-databind-2.13.3.jar:2.13.3]
at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:28) ~[jackson-databind-2.13.3.jar:2.13.3]
at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:323) ~[jackson-databind-2.13.3.jar:2.13.3]
at com.fasterxml.jackson.databind.ObjectReader._bindAndClose(ObjectReader.java:2051) ~[jackson-databind-2.13.3.jar:2.13.3]
at com.fasterxml.jackson.databind.ObjectReader.readValue(ObjectReader.java:1529) ~[jackson-databind-2.13.3.jar:2.13.3]
at org.axonframework.serialization.json.JacksonSerializer.deserialize(JacksonSerializer.java:201) ~[axon-messaging-4.5.15.jar:4.5.15]
at org.axonframework.serialization.LazyDeserializingObject.getObject(LazyDeserializingObject.java:102) ~[axon-messaging-4.5.15.jar:4.5.15]
at org.axonframework.axonserver.connector.query.GrpcBackedResponseMessage.getPayload(GrpcBackedResponseMessage.java:99) ~[axon-server-connector-4.5.15.jar:4.5.15]
at org.axonframework.messaging.responsetypes.ConvertingResponseMessage.getPayload(ConvertingResponseMessage.java:85) ~[axon-messaging-4.5.15.jar:4.5.15]
at org.axonframework.queryhandling.DefaultQueryGateway.lambda$query$1(DefaultQueryGateway.java:87) ~[axon-messaging-4.5.15.jar:4.5.15]
at java.base/java.util.concurrent.CompletableFuture$UniAccept.tryFire(CompletableFuture.java:718) ~[na:na]
at java.base/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:510) ~[na:na]
at java.base/java.util.concurrent.CompletableFuture.complete(CompletableFuture.java:2147) ~[na:na]
at org.axonframework.axonserver.connector.query.AxonServerQueryBus$ResponseProcessingTask.run(AxonServerQueryBus.java:761) ~[axon-server-connector-4.5.15.jar:4.5.15]
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136) ~[na:na]
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635) ~[na:na]
at java.base/java.lang.Thread.run(Thread.java:833) ~[na:na]
The new config
@Bean
@Qualifier("mainObjectMapper")
@Primary
fun createMapper(): ObjectMapper {
val builder = Jackson2ObjectMapperBuilder()
builder.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
return builder.build<ObjectMapper>()
.registerKotlinModule()
.registerModule(
SimpleModule()
.addSerializer(Id::class.javaObjectType, IdJsonSerializer())
.addSerializer(Id::class.javaPrimitiveType, IdJsonSerializer())
.addDeserializer(Id::class.javaObjectType, IdJsonDeserializer())
.addDeserializer(Id::class.javaPrimitiveType, IdJsonDeserializer())
)
.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY)
}
@Bean
@Primary
fun axonJacksonSerializer(objectMapper: ObjectMapper): Serializer =
JacksonSerializer.builder()
.objectMapper(objectMapper)
.defaultTyping()
.build()
Also I am using this config:
axon:
serializer:
general: jackson
events: jackson
messages: jackson
Solution
Jackson loses its type information when using the default ObjectMapper
settings. Axon has a method to easily configure including list information into the JSON. You can adjust your bean definition to this:
@Bean
@Primary
fun axonJacksonSerializer(objectMapper: ObjectMapper): Serializer =
JacksonSerializer.builder()
.objectMapper(objectMapper)
.defaultTyping()
.build()
Calling the defaultTyping()
on the builder will set the appropriate setting on the ObjectMapper
. You can find more information on the topic here
Answered By - Mitchell Herrijgers
Answer Checked By - Mildred Charles (JavaFixing Admin)