Skip to content

Commit 204e800

Browse files
[#368] AxonSerializer documentation and enforce ReplayToken.context to String (#370)
Co-authored-by: Steven van Beelen <[email protected]>
1 parent 6a44b9f commit 204e800

File tree

2 files changed

+146
-10
lines changed

2 files changed

+146
-10
lines changed

kotlin/src/main/kotlin/org/axonframework/extensions/kotlin/serialization/AxonSerializers.kt

+105-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2010-2024. Axon Framework
2+
* Copyright (c) 2010-2025. Axon Framework
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -49,8 +49,33 @@ import org.axonframework.messaging.responsetypes.OptionalResponseType
4949
import org.axonframework.messaging.responsetypes.ResponseType
5050
import kotlin.reflect.KClass
5151

52-
private val trackingTokenSerializer = PolymorphicSerializer(TrackingToken::class).nullable
52+
/**
53+
* Serializer for Axon's [TrackingToken] class.
54+
* Provides serialization and deserialization support for nullable instances of TrackingToken.
55+
*
56+
* @see TrackingToken
57+
*/
58+
val trackingTokenSerializer = PolymorphicSerializer(TrackingToken::class).nullable
5359

60+
/**
61+
* Serializer for the [ReplayToken.context], represented as a nullable String.
62+
* This context is typically used to provide additional information during token replay operations.
63+
*
64+
* This serializer is used by [trackingTokenSerializer] to serialize the context field and now only [String] type or null value is supported!
65+
* Sadly enough, there's no straightforward solution to support [Any]; not without adjusting the context field of the ReplayToken in Axon Framework itself.
66+
* That is, however, a breaking change, and as such, cannot be done till version 5.0.0 of the Axon Framework.
67+
* This also allow more complex objects as the context, although it requires the user to do the de-/serialization to/from String, instead of the Axon Framework itself.
68+
* Look at AxonSerializersTest, case `replay token with complex object as String context` for an example how to handle that using Kotlin Serialization.
69+
*
70+
* @see ReplayToken.context
71+
*/
72+
val replayTokenContextSerializer = String.serializer().nullable
73+
74+
/**
75+
* Module defining serializers for Axon Framework's core event handling and messaging components.
76+
* This module includes serializers for TrackingTokens, ScheduleTokens, and ResponseTypes, enabling
77+
* seamless integration with Axon-based applications.
78+
*/
5479
val AxonSerializersModule = SerializersModule {
5580
contextual(ConfigToken::class) { ConfigTokenSerializer }
5681
contextual(GapAwareTrackingToken::class) { GapAwareTrackingTokenSerializer }
@@ -86,6 +111,11 @@ val AxonSerializersModule = SerializersModule {
86111
}
87112
}
88113

114+
/**
115+
* Serializer for [ConfigToken].
116+
*
117+
* @see ConfigToken
118+
*/
89119
object ConfigTokenSerializer : KSerializer<ConfigToken> {
90120

91121
private val mapSerializer = MapSerializer(String.serializer(), String.serializer())
@@ -112,6 +142,11 @@ object ConfigTokenSerializer : KSerializer<ConfigToken> {
112142
}
113143
}
114144

145+
/**
146+
* Serializer for [GapAwareTrackingToken].
147+
*
148+
* @see GapAwareTrackingToken
149+
*/
115150
object GapAwareTrackingTokenSerializer : KSerializer<GapAwareTrackingToken> {
116151

117152
private val setSerializer = SetSerializer(Long.serializer())
@@ -143,6 +178,11 @@ object GapAwareTrackingTokenSerializer : KSerializer<GapAwareTrackingToken> {
143178
}
144179
}
145180

181+
/**
182+
* Serializer for [MultiSourceTrackingToken].
183+
*
184+
* @see MultiSourceTrackingToken
185+
*/
146186
object MultiSourceTrackingTokenSerializer : KSerializer<MultiSourceTrackingToken> {
147187

148188
private val mapSerializer = MapSerializer(String.serializer(), trackingTokenSerializer)
@@ -169,6 +209,11 @@ object MultiSourceTrackingTokenSerializer : KSerializer<MultiSourceTrackingToken
169209
}
170210
}
171211

212+
/**
213+
* Serializer for [MergedTrackingToken].
214+
*
215+
* @see MergedTrackingToken
216+
*/
172217
object MergedTrackingTokenSerializer : KSerializer<MergedTrackingToken> {
173218

174219
override val descriptor = buildClassSerialDescriptor(MergedTrackingToken::class.java.name) {
@@ -199,36 +244,62 @@ object MergedTrackingTokenSerializer : KSerializer<MergedTrackingToken> {
199244
}
200245
}
201246

247+
/**
248+
* Serializer for [ReplayToken].
249+
* The [ReplayToken.context] value can be only a String or null.
250+
* This serializer uses [replayTokenContextSerializer] to serialize the context field and now only [String] type or null value is supported!
251+
*
252+
* @see ReplayToken
253+
* @see [replayTokenContextSerializer]
254+
*/
202255
object ReplayTokenSerializer : KSerializer<ReplayToken> {
203256

204257
override val descriptor = buildClassSerialDescriptor(ReplayToken::class.java.name) {
205258
element<TrackingToken>("tokenAtReset")
206259
element<TrackingToken>("currentToken")
260+
element<String>("context")
207261
}
208262

209263
override fun deserialize(decoder: Decoder) = decoder.decodeStructure(descriptor) {
210264
var tokenAtReset: TrackingToken? = null
211265
var currentToken: TrackingToken? = null
266+
var context: String? = null
212267
while (true) {
213268
val index = decodeElementIndex(descriptor)
214269
if (index == CompositeDecoder.DECODE_DONE) break
215270
when (index) {
216271
0 -> tokenAtReset = decodeSerializableElement(descriptor, index, trackingTokenSerializer)
217272
1 -> currentToken = decodeSerializableElement(descriptor, index, trackingTokenSerializer)
273+
2 -> context = decodeSerializableElement(descriptor, index, replayTokenContextSerializer)
218274
}
219275
}
220-
ReplayToken(
276+
ReplayToken.createReplayToken(
221277
tokenAtReset ?: throw SerializationException("Element 'tokenAtReset' is missing"),
222278
currentToken,
223-
)
279+
context
280+
) as ReplayToken
224281
}
225282

226283
override fun serialize(encoder: Encoder, value: ReplayToken) = encoder.encodeStructure(descriptor) {
227284
encodeSerializableElement(descriptor, 0, trackingTokenSerializer, value.tokenAtReset)
228285
encodeSerializableElement(descriptor, 1, trackingTokenSerializer, value.currentToken)
286+
encodeSerializableElement(
287+
descriptor,
288+
2,
289+
replayTokenContextSerializer,
290+
stringOrNullFrom(value.context())
291+
)
229292
}
293+
294+
private fun stringOrNullFrom(obj: Any?): String? =
295+
obj?.takeIf { it is String }?.let { it as String }
230296
}
231297

298+
/**
299+
* Serializer for [GlobalSequenceTrackingToken].
300+
*
301+
* @see GlobalSequenceTrackingToken
302+
*/
232303
object GlobalSequenceTrackingTokenSerializer : KSerializer<GlobalSequenceTrackingToken> {
233304

234305
override val descriptor = buildClassSerialDescriptor(GlobalSequenceTrackingToken::class.java.name) {
@@ -254,6 +325,11 @@ object GlobalSequenceTrackingTokenSerializer : KSerializer<GlobalSequenceTrackin
254325
}
255326
}
256327

328+
/**
329+
* Serializer for [SimpleScheduleToken].
330+
*
331+
* @see SimpleScheduleToken
332+
*/
257333
object SimpleScheduleTokenSerializer : KSerializer<SimpleScheduleToken> {
258334

259335
override val descriptor = buildClassSerialDescriptor(SimpleScheduleToken::class.java.name) {
@@ -279,6 +355,11 @@ object SimpleScheduleTokenSerializer : KSerializer<SimpleScheduleToken> {
279355
}
280356
}
281357

358+
/**
359+
* Serializer for [QuartzScheduleToken].
360+
*
361+
* @see QuartzScheduleToken
362+
*/
282363
object QuartzScheduleTokenSerializer : KSerializer<QuartzScheduleToken> {
283364

284365
override val descriptor = buildClassSerialDescriptor(QuartzScheduleToken::class.java.name) {
@@ -334,14 +415,34 @@ abstract class ResponseTypeSerializer<R : ResponseType<*>>(kClass: KClass<R>, pr
334415
}
335416
}
336417

418+
/**
419+
* Serializer for [InstanceResponseType].
420+
*
421+
* @see InstanceResponseType
422+
*/
337423
object InstanceResponseTypeSerializer : KSerializer<InstanceResponseType<*>>,
338424
ResponseTypeSerializer<InstanceResponseType<*>>(InstanceResponseType::class, { InstanceResponseType(it) })
339425

426+
/**
427+
* Serializer for [OptionalResponseType].
428+
*
429+
* @see OptionalResponseType
430+
*/
340431
object OptionalResponseTypeSerializer : KSerializer<OptionalResponseType<*>>,
341432
ResponseTypeSerializer<OptionalResponseType<*>>(OptionalResponseType::class, { OptionalResponseType(it) })
342433

434+
/**
435+
* Serializer for [MultipleInstancesResponseType].
436+
*
437+
* @see MultipleInstancesResponseType
438+
*/
343439
object MultipleInstancesResponseTypeSerializer : KSerializer<MultipleInstancesResponseType<*>>,
344440
ResponseTypeSerializer<MultipleInstancesResponseType<*>>(MultipleInstancesResponseType::class, { MultipleInstancesResponseType(it) })
345441

442+
/**
443+
* Serializer for [ArrayResponseType].
444+
*
445+
* @see ArrayResponseType
446+
*/
346447
object ArrayResponseTypeSerializer : KSerializer<ArrayResponseType<*>>,
347448
ResponseTypeSerializer<ArrayResponseType<*>>(ArrayResponseType::class, { ArrayResponseType(it) })

kotlin/src/test/kotlin/org/axonframework/extensions/kotlin/serializer/AxonSerializersTest.kt

+41-6
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@
1515
*/
1616
package org.axonframework.extensions.kotlin.serializer
1717

18+
import com.fasterxml.jackson.databind.ObjectMapper
19+
import com.fasterxml.jackson.module.kotlin.KotlinModule
20+
import kotlinx.serialization.Serializable
21+
import kotlinx.serialization.decodeFromString
22+
import kotlinx.serialization.encodeToString
1823
import kotlinx.serialization.json.Json
1924
import org.axonframework.eventhandling.GapAwareTrackingToken
2025
import org.axonframework.eventhandling.GlobalSequenceTrackingToken
@@ -36,7 +41,9 @@ import org.axonframework.messaging.responsetypes.ResponseType
3641
import org.axonframework.serialization.Serializer
3742
import org.axonframework.serialization.SimpleSerializedObject
3843
import org.axonframework.serialization.SimpleSerializedType
44+
import org.axonframework.serialization.json.JacksonSerializer
3945
import org.junit.jupiter.api.Assertions.assertEquals
46+
import org.junit.jupiter.api.Assertions.assertInstanceOf
4047
import org.junit.jupiter.api.Test
4148

4249
internal class AxonSerializersTest {
@@ -76,21 +83,49 @@ internal class AxonSerializersTest {
7683
}
7784

7885
@Test
79-
fun replayToken() {
80-
val token = ReplayToken.createReplayToken(GlobalSequenceTrackingToken(15), GlobalSequenceTrackingToken(10))
81-
val json = """{"tokenAtReset":{"type":"org.axonframework.eventhandling.GlobalSequenceTrackingToken","globalIndex":15},"currentToken":{"type":"org.axonframework.eventhandling.GlobalSequenceTrackingToken","globalIndex":10}}"""
86+
fun `replay token with String context`() {
87+
val token = ReplayToken.createReplayToken(
88+
GlobalSequenceTrackingToken(15), GlobalSequenceTrackingToken(10), "someContext"
89+
)
90+
val json = """{"tokenAtReset":{"type":"org.axonframework.eventhandling.GlobalSequenceTrackingToken","globalIndex":15},"currentToken":{"type":"org.axonframework.eventhandling.GlobalSequenceTrackingToken","globalIndex":10},"context":"someContext"}""".trimIndent()
8291
assertEquals(json, serializer.serialize(token, String::class.java).data)
8392
assertEquals(token, serializer.deserializeTrackingToken(token.javaClass.name, json))
8493
}
8594

8695
@Test
87-
fun `replay token with currentToken with null value`() {
88-
val token = ReplayToken.createReplayToken(GlobalSequenceTrackingToken(5), null)
89-
val json = """{"tokenAtReset":{"type":"org.axonframework.eventhandling.GlobalSequenceTrackingToken","globalIndex":5},"currentToken":null}"""
96+
fun `replay token with currentToken with null value and null context`() {
97+
val token = ReplayToken.createReplayToken(GlobalSequenceTrackingToken(5), null, null)
98+
val json = """{"tokenAtReset":{"type":"org.axonframework.eventhandling.GlobalSequenceTrackingToken","globalIndex":5},"currentToken":null,"context":null}"""
9099
assertEquals(json, serializer.serialize(token, String::class.java).data)
91100
assertEquals(token, serializer.deserializeTrackingToken(token.javaClass.name, json))
92101
}
93102

103+
@Test
104+
fun `replay token deserialize without context field`() {
105+
val token = ReplayToken.createReplayToken(GlobalSequenceTrackingToken(5), null, null)
106+
val json = """{"tokenAtReset":{"type":"org.axonframework.eventhandling.GlobalSequenceTrackingToken","globalIndex":5},"currentToken":null}"""
107+
assertEquals(token, serializer.deserializeTrackingToken(token.javaClass.name, json))
108+
}
109+
110+
@Test
111+
fun `replay token with complex object as String context`() {
112+
@Serializable
113+
data class ComplexContext(val value1: String, val value2: Int, val value3: Boolean)
114+
val complexContext = ComplexContext("value1", 2, false)
115+
116+
val token = ReplayToken.createReplayToken(
117+
GlobalSequenceTrackingToken(15),
118+
GlobalSequenceTrackingToken(10),
119+
Json.encodeToString(complexContext)
120+
)
121+
val json = """{"tokenAtReset":{"type":"org.axonframework.eventhandling.GlobalSequenceTrackingToken","globalIndex":15},"currentToken":{"type":"org.axonframework.eventhandling.GlobalSequenceTrackingToken","globalIndex":10},"context":"{\"value1\":\"value1\",\"value2\":2,\"value3\":false}"}""".trimIndent()
122+
assertEquals(json, serializer.serialize(token, String::class.java).data)
123+
val deserializedToken = serializer.deserializeTrackingToken(token.javaClass.name, json) as ReplayToken
124+
assertEquals(token, deserializedToken)
125+
assertInstanceOf(String::class.java, deserializedToken.context())
126+
assertEquals(complexContext, Json.decodeFromString<ComplexContext>(deserializedToken.context() as String))
127+
}
128+
94129
@Test
95130
fun globalSequenceTrackingToken() {
96131
val token = GlobalSequenceTrackingToken(5)

0 commit comments

Comments
 (0)