Search code examples
springspring-bootkotlinaxon

Axon query response not convertible using Jackson and Kotlin


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

enter image description here

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