Skip to content

Commit d69cca0

Browse files
authored
Merge pull request #381 from AxonFramework/fix/#380_metadata_serializer
[#380] fix: Kotlin Serializer not available for org.axonframework.messaging.MetaData
2 parents dc291b0 + a14f3df commit d69cca0

File tree

5 files changed

+346
-14
lines changed

5 files changed

+346
-14
lines changed

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

+4-2
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import org.axonframework.eventhandling.scheduling.java.SimpleScheduleToken
4343
import org.axonframework.eventhandling.scheduling.quartz.QuartzScheduleToken
4444
import org.axonframework.eventhandling.tokenstore.ConfigToken
4545
import org.axonframework.extensions.kotlin.messaging.responsetypes.ArrayResponseType
46+
import org.axonframework.messaging.MetaData
4647
import org.axonframework.messaging.responsetypes.InstanceResponseType
4748
import org.axonframework.messaging.responsetypes.MultipleInstancesResponseType
4849
import org.axonframework.messaging.responsetypes.OptionalResponseType
@@ -73,7 +74,7 @@ val replayTokenContextSerializer = String.serializer().nullable
7374

7475
/**
7576
* 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+
* This module includes serializers for TrackingTokens, ScheduleTokens, ResponseTypes and MetaData enabling
7778
* seamless integration with Axon-based applications.
7879
*/
7980
val AxonSerializersModule = SerializersModule {
@@ -109,6 +110,7 @@ val AxonSerializersModule = SerializersModule {
109110
subclass(MultipleInstancesResponseTypeSerializer)
110111
subclass(ArrayResponseTypeSerializer)
111112
}
113+
contextual(MetaData::class) { MetaDataSerializer }
112114
}
113115

114116
/**
@@ -445,4 +447,4 @@ object MultipleInstancesResponseTypeSerializer : KSerializer<MultipleInstancesRe
445447
* @see ArrayResponseType
446448
*/
447449
object ArrayResponseTypeSerializer : KSerializer<ArrayResponseType<*>>,
448-
ResponseTypeSerializer<ArrayResponseType<*>>(ArrayResponseType::class, { ArrayResponseType(it) })
450+
ResponseTypeSerializer<ArrayResponseType<*>>(ArrayResponseType::class, { ArrayResponseType(it) })
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package org.axonframework.extensions.kotlin.serialization
2+
3+
import kotlinx.serialization.KSerializer
4+
import kotlinx.serialization.SerializationException
5+
import kotlinx.serialization.builtins.MapSerializer
6+
import kotlinx.serialization.builtins.serializer
7+
import kotlinx.serialization.descriptors.SerialDescriptor
8+
import kotlinx.serialization.encoding.Decoder
9+
import kotlinx.serialization.encoding.Encoder
10+
import kotlinx.serialization.json.*
11+
import org.axonframework.messaging.MetaData
12+
import java.time.Instant
13+
import java.util.UUID
14+
15+
/**
16+
* A Kotlinx [KSerializer] for Axon Framework's [MetaData] type, supporting serialization across any format.
17+
*
18+
* This serializer converts a [MetaData] instance to a JSON-encoded [String] using a recursive conversion
19+
* of all entries into [JsonElement]s. This JSON string is then serialized using [String.serializer],
20+
* ensuring compatibility with any [kotlinx.serialization.encoding.Encoder]—including formats such as JSON, CBOR, ProtoBuf, or Avro.
21+
*
22+
* ### Supported value types
23+
* Each entry in the MetaData map must conform to one of the following:
24+
* - Primitives: [String], [Int], [Long], [Float], [Double], [Boolean]
25+
* - Complex types: [UUID], [Instant]
26+
* - Collections: [Collection], [List], [Set]
27+
* - Arrays: [Array]
28+
* - Nested Maps: [Map] with keys convertible to [String]
29+
*
30+
* ### Limitations
31+
* - Custom types that do not fall into the above categories will throw a [SerializationException]
32+
* - Deserialized non-primitive types (like [UUID], [Instant]) are restored as [String], not their original types
33+
*
34+
* This serializer guarantees structural integrity of nested metadata (e.g. map within list within map), while remaining format-agnostic.
35+
*
36+
* @author Mateusz Nowak
37+
* @since 4.11.1
38+
*/
39+
object MetaDataSerializer : KSerializer<MetaData> {
40+
41+
private val json = Json { encodeDefaults = true; ignoreUnknownKeys = true }
42+
43+
override val descriptor: SerialDescriptor = String.serializer().descriptor
44+
45+
override fun serialize(encoder: Encoder, value: MetaData) {
46+
val map: Map<String, JsonElement> = value.entries.associate { (key, rawValue) ->
47+
key to toJsonElement(rawValue)
48+
}
49+
val jsonString = json.encodeToString(MapSerializer(String.serializer(), JsonElement.serializer()), map)
50+
encoder.encodeSerializableValue(String.serializer(), jsonString)
51+
}
52+
53+
override fun deserialize(decoder: Decoder): MetaData {
54+
val jsonString = decoder.decodeSerializableValue(String.serializer())
55+
val map = json.decodeFromString(MapSerializer(String.serializer(), JsonElement.serializer()), jsonString)
56+
val reconstructed = map.mapValues { (_, jsonElement) -> fromJsonElement(jsonElement) }
57+
return MetaData(reconstructed)
58+
}
59+
60+
private fun toJsonElement(value: Any?): JsonElement = when (value) {
61+
null -> JsonNull
62+
is String -> JsonPrimitive(value)
63+
is Boolean -> JsonPrimitive(value)
64+
is Int -> JsonPrimitive(value)
65+
is Long -> JsonPrimitive(value)
66+
is Float -> JsonPrimitive(value)
67+
is Double -> JsonPrimitive(value)
68+
is UUID -> JsonPrimitive(value.toString())
69+
is Instant -> JsonPrimitive(value.toString())
70+
is Map<*, *> -> JsonObject(value.entries.associate { (k, v) -> k.toString() to toJsonElement(v) })
71+
is Collection<*> -> JsonArray(value.map { toJsonElement(it) })
72+
is Array<*> -> JsonArray(value.map { toJsonElement(it) })
73+
else -> throw SerializationException("Unsupported type: ${value::class}")
74+
}
75+
76+
private fun fromJsonElement(element: JsonElement): Any? = when (element) {
77+
is JsonNull -> null
78+
is JsonPrimitive -> {
79+
if (element.isString) {
80+
element.content
81+
} else {
82+
element.booleanOrNull
83+
?: element.intOrNull
84+
?: element.longOrNull
85+
?: element.floatOrNull
86+
?: element.doubleOrNull
87+
?: element.content
88+
}
89+
}
90+
is JsonObject -> element.mapValues { fromJsonElement(it.value) }
91+
is JsonArray -> element.map { fromJsonElement(it) }
92+
}
93+
}

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

+3-10
Original file line numberDiff line numberDiff line change
@@ -15,35 +15,28 @@
1515
*/
1616
package org.axonframework.extensions.kotlin.serializer
1717

18-
import com.fasterxml.jackson.databind.ObjectMapper
19-
import com.fasterxml.jackson.module.kotlin.KotlinModule
2018
import kotlinx.serialization.Serializable
21-
import kotlinx.serialization.decodeFromString
2219
import kotlinx.serialization.encodeToString
2320
import kotlinx.serialization.json.Json
24-
import org.axonframework.eventhandling.GapAwareTrackingToken
25-
import org.axonframework.eventhandling.GlobalSequenceTrackingToken
26-
import org.axonframework.eventhandling.MergedTrackingToken
27-
import org.axonframework.eventhandling.MultiSourceTrackingToken
28-
import org.axonframework.eventhandling.ReplayToken
29-
import org.axonframework.eventhandling.TrackingToken
21+
import org.axonframework.eventhandling.*
3022
import org.axonframework.eventhandling.scheduling.ScheduleToken
3123
import org.axonframework.eventhandling.scheduling.java.SimpleScheduleToken
3224
import org.axonframework.eventhandling.scheduling.quartz.QuartzScheduleToken
3325
import org.axonframework.eventhandling.tokenstore.ConfigToken
3426
import org.axonframework.extensions.kotlin.messaging.responsetypes.ArrayResponseType
3527
import org.axonframework.extensions.kotlin.serialization.AxonSerializersModule
3628
import org.axonframework.extensions.kotlin.serialization.KotlinSerializer
29+
import org.axonframework.messaging.MetaData
3730
import org.axonframework.messaging.responsetypes.InstanceResponseType
3831
import org.axonframework.messaging.responsetypes.MultipleInstancesResponseType
3932
import org.axonframework.messaging.responsetypes.OptionalResponseType
4033
import org.axonframework.messaging.responsetypes.ResponseType
4134
import org.axonframework.serialization.Serializer
4235
import org.axonframework.serialization.SimpleSerializedObject
4336
import org.axonframework.serialization.SimpleSerializedType
44-
import org.axonframework.serialization.json.JacksonSerializer
4537
import org.junit.jupiter.api.Assertions.assertEquals
4638
import org.junit.jupiter.api.Assertions.assertInstanceOf
39+
import org.junit.jupiter.api.Nested
4740
import org.junit.jupiter.api.Test
4841

4942
internal class AxonSerializersTest {

0 commit comments

Comments
 (0)