Skip to content

Commit 0775b93

Browse files
MateuszNaKodachsmcvb
authored andcommitted
[#380] refactor: add dedicated JsonMetaDataSerializer
(cherry picked from commit 4880267)
1 parent 18a6b96 commit 0775b93

File tree

3 files changed

+152
-20
lines changed

3 files changed

+152
-20
lines changed

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ val AxonSerializersModule = SerializersModule {
110110
subclass(MultipleInstancesResponseTypeSerializer)
111111
subclass(ArrayResponseTypeSerializer)
112112
}
113-
contextual(MetaData::class) { MetaDataSerializer }
113+
contextual(MetaData::class) { ComposedMetaDataSerializer }
114114
}
115115

116116
/**

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

+128-18
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import kotlinx.serialization.SerializationException
55
import kotlinx.serialization.builtins.MapSerializer
66
import kotlinx.serialization.builtins.serializer
77
import kotlinx.serialization.descriptors.SerialDescriptor
8+
import kotlinx.serialization.encodeToString
89
import kotlinx.serialization.encoding.Decoder
910
import kotlinx.serialization.encoding.Encoder
1011
import kotlinx.serialization.json.*
@@ -13,11 +14,44 @@ import java.time.Instant
1314
import java.util.UUID
1415

1516
/**
16-
* A Kotlinx [KSerializer] for Axon Framework's [MetaData] type, supporting serialization across any format.
17+
* A composite Kotlinx [KSerializer] for Axon Framework's [MetaData] type that selects the
18+
* appropriate serializer based on the encoder/decoder type.
19+
*
20+
* This serializer delegates to:
21+
* - [JsonMetaDataSerializer] when used with [JsonEncoder]/[JsonDecoder]
22+
* - [StringMetaDataSerializer] for all other encoder/decoder types
23+
*
24+
* This allows efficient JSON serialization without unnecessary string encoding, while
25+
* maintaining compatibility with all other serialization formats through string-based
26+
* serialization.
27+
*
28+
* @author Mateusz Nowak
29+
* @since 4.11.2
30+
*/
31+
object ComposedMetaDataSerializer : KSerializer<MetaData> {
32+
override val descriptor: SerialDescriptor = StringMetaDataSerializer.descriptor
33+
34+
override fun serialize(encoder: Encoder, value: MetaData) {
35+
when (encoder) {
36+
is JsonEncoder -> JsonMetaDataSerializer.serialize(encoder, value)
37+
else -> StringMetaDataSerializer.serialize(encoder, value)
38+
}
39+
}
40+
41+
override fun deserialize(decoder: Decoder): MetaData {
42+
return when (decoder) {
43+
is JsonDecoder -> JsonMetaDataSerializer.deserialize(decoder)
44+
else -> StringMetaDataSerializer.deserialize(decoder)
45+
}
46+
}
47+
}
48+
49+
/**
50+
* A Kotlinx [KSerializer] for Axon Framework's [MetaData] type, suitable for serialization across any format.
1751
*
1852
* 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.
53+
* of all entries into [JsonElement]s. This JSON string is then serialized using [String.serializer()],
54+
* ensuring compatibility with any serialization format.
2155
*
2256
* ### Supported value types
2357
* Each entry in the MetaData map must conform to one of the following:
@@ -31,13 +65,12 @@ import java.util.UUID
3165
* - Custom types that do not fall into the above categories will throw a [SerializationException]
3266
* - Deserialized non-primitive types (like [UUID], [Instant]) are restored as [String], not their original types
3367
*
34-
* This serializer guarantees structural integrity of nested metadata (e.g. map within list within map), while remaining format-agnostic.
68+
* This serializer guarantees structural integrity of nested metadata while remaining format-agnostic.
3569
*
3670
* @author Mateusz Nowak
3771
* @since 4.11.1
3872
*/
39-
object MetaDataSerializer : KSerializer<MetaData> {
40-
73+
object StringMetaDataSerializer : KSerializer<MetaData> {
4174
private val json = Json { encodeDefaults = true; ignoreUnknownKeys = true }
4275

4376
override val descriptor: SerialDescriptor = String.serializer().descriptor
@@ -46,14 +79,93 @@ object MetaDataSerializer : KSerializer<MetaData> {
4679
val map: Map<String, JsonElement> = value.entries.associate { (key, rawValue) ->
4780
key to toJsonElement(rawValue)
4881
}
49-
val jsonString = json.encodeToString(MapSerializer(String.serializer(), JsonElement.serializer()), map)
50-
encoder.encodeSerializableValue(String.serializer(), jsonString)
82+
val jsonString = json.encodeToString(JsonObject(map))
83+
encoder.encodeString(jsonString)
84+
}
85+
86+
override fun deserialize(decoder: Decoder): MetaData {
87+
val jsonString = decoder.decodeString()
88+
val jsonObject = json.parseToJsonElement(jsonString).jsonObject
89+
val reconstructed = jsonObject.mapValues { (_, jsonElement) ->
90+
fromJsonElement(jsonElement)
91+
}
92+
return MetaData(reconstructed)
93+
}
94+
95+
private fun toJsonElement(value: Any?): JsonElement = when (value) {
96+
null -> JsonNull
97+
is String -> JsonPrimitive(value)
98+
is Boolean -> JsonPrimitive(value)
99+
is Int -> JsonPrimitive(value)
100+
is Long -> JsonPrimitive(value)
101+
is Float -> JsonPrimitive(value)
102+
is Double -> JsonPrimitive(value)
103+
is UUID -> JsonPrimitive(value.toString())
104+
is Instant -> JsonPrimitive(value.toString())
105+
is Map<*, *> -> JsonObject(value.entries.associate { (k, v) ->
106+
k.toString() to toJsonElement(v)
107+
})
108+
is Collection<*> -> JsonArray(value.map { toJsonElement(it) })
109+
is Array<*> -> JsonArray(value.map { toJsonElement(it) })
110+
else -> throw SerializationException("Unsupported type: ${value::class}")
111+
}
112+
113+
private fun fromJsonElement(element: JsonElement): Any? = when (element) {
114+
is JsonNull -> null
115+
is JsonPrimitive -> {
116+
if (element.isString) {
117+
element.content
118+
} else {
119+
element.booleanOrNull ?: element.intOrNull ?: element.longOrNull ?:
120+
element.floatOrNull ?: element.doubleOrNull ?: element.content
121+
}
122+
}
123+
is JsonObject -> element.mapValues { fromJsonElement(it.value) }
124+
is JsonArray -> element.map { fromJsonElement(it) }
125+
}
126+
}
127+
128+
/**
129+
* A Kotlinx [KSerializer] for Axon Framework's [MetaData] type, optimized for JSON serialization.
130+
*
131+
* This serializer converts a [MetaData] instance directly to a JSON object structure,
132+
* avoiding the string-encoding that [StringMetaDataSerializer] uses. This ensures JSON values
133+
* are properly encoded without quote escaping.
134+
*
135+
* ### Supported value types
136+
* Each entry in the MetaData map must conform to one of the following:
137+
* - Primitives: [String], [Int], [Long], [Float], [Double], [Boolean]
138+
* - Complex types: [UUID], [Instant]
139+
* - Collections: [Collection], [List], [Set]
140+
* - Arrays: [Array]
141+
* - Nested Maps: [Map] with keys convertible to [String]
142+
*
143+
* ### Limitations
144+
* - Custom types that do not fall into the above categories will throw a [SerializationException]
145+
* - Deserialized non-primitive types (like [UUID], [Instant]) are restored as [String], not their original types
146+
*
147+
* This serializer is specifically optimized for JSON serialization formats.
148+
*
149+
* @author Mateusz Nowak
150+
* @since 4.11.2
151+
*/
152+
object JsonMetaDataSerializer : KSerializer<MetaData> {
153+
private val mapSerializer = MapSerializer(String.serializer(), JsonElement.serializer())
154+
155+
override val descriptor: SerialDescriptor = mapSerializer.descriptor
156+
157+
override fun serialize(encoder: Encoder, value: MetaData) {
158+
val jsonMap = value.entries.associate { (key, rawValue) ->
159+
key to toJsonElement(rawValue)
160+
}
161+
encoder.encodeSerializableValue(mapSerializer, jsonMap)
51162
}
52163

53164
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) }
165+
val jsonMap = decoder.decodeSerializableValue(mapSerializer)
166+
val reconstructed = jsonMap.mapValues { (_, jsonElement) ->
167+
fromJsonElement(jsonElement)
168+
}
57169
return MetaData(reconstructed)
58170
}
59171

@@ -67,7 +179,9 @@ object MetaDataSerializer : KSerializer<MetaData> {
67179
is Double -> JsonPrimitive(value)
68180
is UUID -> JsonPrimitive(value.toString())
69181
is Instant -> JsonPrimitive(value.toString())
70-
is Map<*, *> -> JsonObject(value.entries.associate { (k, v) -> k.toString() to toJsonElement(v) })
182+
is Map<*, *> -> JsonObject(value.entries.associate { (k, v) ->
183+
k.toString() to toJsonElement(v)
184+
})
71185
is Collection<*> -> JsonArray(value.map { toJsonElement(it) })
72186
is Array<*> -> JsonArray(value.map { toJsonElement(it) })
73187
else -> throw SerializationException("Unsupported type: ${value::class}")
@@ -79,12 +193,8 @@ object MetaDataSerializer : KSerializer<MetaData> {
79193
if (element.isString) {
80194
element.content
81195
} else {
82-
element.booleanOrNull
83-
?: element.intOrNull
84-
?: element.longOrNull
85-
?: element.floatOrNull
86-
?: element.doubleOrNull
87-
?: element.content
196+
element.booleanOrNull ?: element.intOrNull ?: element.longOrNull ?:
197+
element.floatOrNull ?: element.doubleOrNull ?: element.content
88198
}
89199
}
90200
is JsonObject -> element.mapValues { fromJsonElement(it.value) }

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

+23-1
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,28 @@ class MetaDataSerializerTest {
218218
assertEquals(deserializedValue, complexStructure)
219219
}
220220

221+
@Test
222+
fun `should not escape quotes in complex nested structures in MetaData`() {
223+
val complexStructure = mapOf(
224+
"string" to "value",
225+
"number" to 42,
226+
"boolean" to true,
227+
"null" to null,
228+
"list" to listOf(1, 2, 3),
229+
"nestedMap" to mapOf(
230+
"a" to "valueA",
231+
"b" to listOf("x", "y", "z"),
232+
"c" to mapOf("nested" to "deepValue")
233+
)
234+
)
235+
236+
val metaData = MetaData.with("complexValue", complexStructure)
237+
238+
val serialized = jsonSerializer.serialize(metaData, String::class.java)
239+
val json = """{"complexValue":{"string":"value","number":42,"boolean":true,"null":null,"list":[1,2,3],"nestedMap":{"a":"valueA","b":["x","y","z"],"c":{"nested":"deepValue"}}}}"""
240+
assertEquals(json, serialized.data);
241+
}
242+
221243
@Test
222244
fun `do not handle custom objects`() {
223245
data class Person(val name: String, val age: Int)
@@ -235,7 +257,7 @@ class MetaDataSerializerTest {
235257
fun `should throw exception when deserializing malformed JSON`() {
236258
val serializedType = SimpleSerializedType(MetaData::class.java.name, null)
237259

238-
val syntaxErrorJson = """{"key": value}""" // missing quotes around value
260+
val syntaxErrorJson = """{"key": value""" // missing closing bracket around value
239261
val syntaxErrorObject = SimpleSerializedObject(syntaxErrorJson, String::class.java, serializedType)
240262

241263
assertThrows<SerializationException> {

0 commit comments

Comments
 (0)