1
votes

While testing event migration in axon I found out that new fields with a default value are set to null in fact. What would be a good way to actually add a field (with default value) to previous events? I do not want to have a MyEventV2 where the value is mandatory (no default value) and do not want to specify an "upcaster".

First, we have this event and it was published and stored in axon server:

data class MyEvent(
    val someId: String,
    val name: String
)

Later we change MyEvent, we give a default value to keep compatibility with previous events persisted, we expect old events to have this default value. The real result:'newField' is null when receiving old events.

data class MyEvent(
    val someId: String,
    val name: String,
    val newField: String = "defaultValueButWillBeNullInPreviousEvents... -> would need it to have the actual default value"
)

I tested with axon server, axon 4.2, spring boot 2.2.0, all defaults from axon boot starter, Jackson 2.10.0

Should we use something like (but not great at all...):

data class MyEvent(
    val someId: String,
    val name: String,
    private val newFieldV2: String? = null
) {
    val newField: String
        get() = newFieldV2 ?: "my default value"
}

I have not checked how axon rebuilds events from the event store but I guess it does it via field access (reflection) instead of the constructor. But strange as there is not default empty constructor provided with data class...

EDIT: Axon uses XStreamSerializer by default, I have switched configuration to use Jackson but the issue still there, and the Jackson kotlin module is registered.

EDIT2: Updated to Jackson 2.10.1 (see issue) which should fix but for some reason I get another error "com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of org.x.x.MyEvent (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)".

Error appears if add "ParameterNamesModule", if not added then still get null for the default value of the event.

EDIT3: I wrote tests but axon still has errors with event org.axonframework.serialization.json.JacksonSerializer

@ExperimentalStdlibApi
class JsonDefaultValueTest {

    private lateinit var mapper1: ObjectMapper
    private lateinit var mapper2: ObjectMapper
    private lateinit var mapper3: ObjectMapper
    private lateinit var mapper4: ObjectMapper
    private lateinit var mapper5: ObjectMapper
    private lateinit var mapper6: ObjectMapper

    @BeforeEach
    fun setup() {
        mapper1 = Jackson2ObjectMapperBuilder.json()
            .build()
        mapper2 = ObjectMapper()
        mapper3 = ObjectMapper().registerModule(KotlinModule())
        mapper4 = ObjectMapper().registerModule(KotlinModule(nullisSameAsDefault = true))
        mapper5 = ObjectMapper().registerModule(ParameterNamesModule())
        mapper6 = ObjectMapper()
            .registerModule(ParameterNamesModule())
            .registerModule(KotlinModule())
            .registerModule(Jdk8Module())
            .registerModule(JavaTimeModule())
    }

    @Test
    fun `only s1 passed with mapper 1`() {
        val json = """{"s1":"only s1"}"""

        val event = mapper1.readValue<MyEvent>(json)

        val event2 = mapper1.readerFor(MyEvent::class.java).readValue<MyEvent>(json.encodeToByteArray())

        val expected = MyEvent(
            "only s1",
            aInt = 0
        )

        Assertions.assertEquals(expected, event)
    }

    @Test
    fun `only s1 passed with mapper 2`() {
        val json = """{"s1":"only s1"}"""

        assertThrows<InvalidDefinitionException> { mapper2.readValue<MyEvent>(json) }

        assertThrows<InvalidDefinitionException> { mapper2.readerFor(MyEvent::class.java).readValue<MyEvent>(json.encodeToByteArray()) }

    }

    @Test
    fun `only s1 passed with mapper 3`() {
        val json = """{"s1":"only s1"}"""

        val event = mapper3.readValue<MyEvent>(json)

        val event2 = mapper3.readerFor(MyEvent::class.java).readValue<MyEvent>(json.encodeToByteArray())

        val expected = MyEvent(
            "only s1",
            aInt = 0
        )

        Assertions.assertEquals(expected, event)
        Assertions.assertEquals(expected, event2)
    }

    @Test
    fun `only s1 passed with mapper 4`() {
        val json = """{"s1":"only s1"}"""

        val event = mapper4.readValue<MyEvent>(json)

        val event2 = mapper4.readerFor(MyEvent::class.java).readValue<MyEvent>(json.encodeToByteArray())

        val expected = MyEvent(
            "only s1",
            aInt = 0
        )

        Assertions.assertEquals(expected, event)
        Assertions.assertEquals(expected, event2)
    }

    @Test
    fun `only s1 passed with mapper 5`() {
        val json = """{"s1":"only s1","s2":null}"""

        assertThrows<ValueInstantiationException> { mapper5.readValue<MyEvent>(json) }

        assertThrows<ValueInstantiationException> { mapper5.readerFor(MyEvent::class.java).readValue<MyEvent>(json.encodeToByteArray()) }

    }

    @Test
    fun `only s1 passed with mapper 6`() {
        val json = """{"s1":"only s1"}"""

        val event = mapper6.readValue<MyEvent>(json)

        val event2 = mapper6.readerFor(MyEvent::class.java).readValue<MyEvent>(json.encodeToByteArray())

        val expected = MyEvent(
            "only s1",
            aInt = 0
        )

        Assertions.assertEquals(expected, event)
        Assertions.assertEquals(expected, event2)
    }

    data class MyEvent(
        val s1: String,
        val aInt: Int,
        val s2: String = "my default"
    )
}
2

2 Answers

3
votes

This appears to be a Jackson-Kotlin issue. You need to explicitly configure Jackson to consider "null" (or absence of fields) as "use the default value".

As recommneded in https://github.com/FasterXML/jackson-module-kotlin/issues/130#issuecomment-546625625, you should use:

ObjectMapper().registerModule(KotlinModule(nullisSameAsDefault = true)

0
votes

This sounds like an incorrect configuration of your Jackson ObjectMapper used to deserialize events. In order for it to work with Kotlin-specific features such as default values, you will need to register the Kotlin module: https://github.com/FasterXML/jackson-module-kotlin

val mapper = ObjectMapper().registerModule(KotlinModule())