-
Notifications
You must be signed in to change notification settings - Fork 9
Add kotlin.serialization
implementation of Serializer
#124
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
…to kotlin-serializer
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bunch of comments on here. The biggest one is that it does not contain how this Serializer
should be used for Axon's internal objects. For this to be useable within Axon, I am afraid it must support Axon's objects like the Message
, TrackingToken
, and SagaEntry
.
kotlin/src/main/kotlin/org/axonframework/extensions/kotlin/serialization/KotlinSerializer.kt
Show resolved
Hide resolved
kotlin/src/main/kotlin/org/axonframework/extensions/kotlin/serialization/KotlinSerializer.kt
Outdated
Show resolved
Hide resolved
* @see KotlinSerializer | ||
* @see kotlinSerializer | ||
*/ | ||
class KotlinSerializerConfiguration { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the driving force to use this format instead of a constructor with default values?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These properties are all var
, so mutable. This makes a clean DSL possible, without invoking the constructor directly. But the DSL is totally optional and I could remove it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Honestly, I wouldn't know what's more "Kotlin-like".
If I'd look at the other Axon components, the settings are done through the Builder of a piece of infrastructure.
We've quite recently allowed Builder configuration directly through Axon's overall Configurer
(for the PooledStreamingEventProcessor
, to be exact), which we feel might be the way forward.
I'd wager that such a separate config object does not align very well with that idea.
However, a stated earlier, I am not aware what would be considered "Kotlin-like".
@sandjelkovic, what's your opinion on this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I removed this Configuration class, and used the constructor with default values. It is no problem to add it back later if it is needed.
The Configuration class could be seen as a builder pattern for a Kotlin-style DSL. The Configuration class contains the same properties as the constructor, and all mutable. In the DSL builder function you could configure the Configuration class (or builder, however you want to call it), and that will then invoke the constructor of the actual (immutable) service.
kotlin/src/main/kotlin/org/axonframework/extensions/kotlin/serialization/KotlinSerializer.kt
Outdated
Show resolved
Hide resolved
typeForClass(type) | ||
) | ||
|
||
else -> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would've assumed the Serializer
could default to byte[]
instead of throwing an exception.
Doing so will have this serializer follow the same paradigm as the JacksonSerializer
and XStreamSerializer
.
Several spots in the framework assume that the expectedRepresentation
is byte[]
.
So enabling this option up would make this serializer more valuable throughout the framework.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pretty confident this will require introducing the registerConverters
method (as seen on line 128 of the JacksonSerializer
too).
This method ensures Json specific ContentTypeConverters
are added to the set-up.
Furthermore, I assume this will warrant introducing content type converters going from JsonElement
to byte[]
, and vice versa.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I totally forgot about the (extra) converters. I will add some defaults to convert between byte arrays <-> Strings and JSON Nodes 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I made the JsonElement
type the exception, just like JsonNode
in the Jackson serializer. All other target types are serialized to String, and then converted with the chaining converter.
By default String, Byte array, Json Element and InputStream already work through service discovery, so that is fine.
If any other converters are needed I can add them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for validating @hiddewie 👍
Adding tests to validate this works (as in, the conversion between those types through this serializer) is a massive plus.
kotlin/src/main/kotlin/org/axonframework/extensions/kotlin/serialization/KotlinSerializer.kt
Outdated
Show resolved
Hide resolved
kotlin/src/main/kotlin/org/axonframework/extensions/kotlin/serialization/KotlinSerializer.kt
Outdated
Show resolved
Hide resolved
kotlin/src/main/kotlin/org/axonframework/extensions/kotlin/serialization/KotlinSerializer.kt
Outdated
Show resolved
Hide resolved
// Class<T>: T must be non-null | ||
val kClass = (this as Class<Any>).kotlin | ||
|
||
val companion = kClass.companionObject |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume that the hard requirement on the @Serializable
annotation makes it pretty tough to use this serializer for Axon objects, like the TrackingToken
.
I've skimmed the Kotlin Serializer documentation somewhat but didn't spot a solution for this just yet.
Do you, perchance, have a solution in mind for this, @hiddewie?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the toughest question :)
As far as I can see, there are two solutions:
- Annotate the serializable types in Axon core with
@Serializable
. This adds the compile time requirement of having the Kotlin compiler plugin dependency in the build process. That may or may not be OK. Although, there are also Jackson annotations in the source code already. In runtime there is no Kotlin requirement. - Create handwritten serializers for every class that needs to be serializable. This is quite some (cumbersome, error prone) work, depending on the number of serializable classes in the Axon core. The serializer code can be maintained in this repository.
To illustrate option 2, I added a small example and custom serializer with test.
The third option is abandoning this approach because the Kotlin serialization is not wanted in the core framework, and it is not worth the effort to maintain the custom serializers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(After thinking about option 1 a bit more: I have not tested adding @Serializable
on a Java class and then running the Kotlin compiler plugin on it. Theoretically it should work but in practice I don't know)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the insights here.
If you'd be at leisure to validate whether it is even possible to add the @Serializable
annotation to a Java class, that would be amazing (and save me some personal investigation. 😅).
In the meantime, I'll start an internal discussion about whether we want to add the @Serializable
annotation to the core of Axon Framework.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried a few things to test how the interoperability of Java classes with Kotlin annotations, and the Kotlin annotation processors work together.
What I did:
- Complie Axon framewok locally (4.6.0-SNAPHSOT)
- Add the Maven Kotlin compiler plugin which would compile the Kotlin annotations into
serializer
functions, see instructions here https://github.com/Kotlin/kotlinx.serialization#maven - Add
@kotlinx.serialization.Serializable
to ConfigToken
What I saw:
- The annotation gets added (just like any other annotation) in the classfile
- No serializer static method is generated.
What I did:
- Copy the Java class and copy it to this repository, with the .java extension.
- Let the Kotlin compiler, with the Kotlin serialization compiler plugin comple the .java class as well
What I saw:
- The annotation gets added in the classfile
- Still no serializer static method is generated.
What I did:
- Let IntelliJ convert the .java file to a .kt Kotlin file (very ugly)
What I saw:
- The annotation gets added in the classfile
- A serializer method is generated.
So my conclusion is that the Kotlin serialization compiler plugin only works for Kotlin source files, and that the .serializer()
static method cannot be generated for Java source files.
That is very unfortunate, I might open an issue with the Kotlin Serialization team to ask if that is supposed to work (ref Kotlin/kotlinx.serialization#1687).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Either way, we can not assume that every class that comes here will have a serializer attached to it (nor be a Kotlin class)
If there is no serializer defined, or just based on types, we can fall back on a registry that we can keep as a map of Type (Class, Class name, etc) and a Serializer. That map could be configurable so someone might add support for classes without @Serializable
like Java Dates, and we can pre-populate it with AF serializers configuration. Or keep the AF serializers in a separate config, but that's a finer detail.
This and
this is what I mean, as we need to pass the serializer explicitly in the very broad generic setting anyway, we might as well keep it somewhere in a config map, similar as to how this cache is doing but is just caching all serializers.
import kotlin.test.assertNull | ||
|
||
class KotlinSerializerTest { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am missing test cases that validate if the KotlinSerializer
can be used for Axon objects, like the Message
, TrackingToken
, and SagaEntry
.
Assuming that's because those objects aren't annotated with the @Serializable
annotation.
However, the Serializer
in Axon must de-/serialize those objects for it to be usable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed. See https://github.com/AxonFramework/extension-kotlin/pull/124/files#r645048334 for the comment about serializing internal classes.
I added one test case for the ConfigToken
, and every internal class that should be serializable can be added in that way, if there is a serializer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gotcha. To make this Serializer
implementation workable for Axon Framework entirely, these tests will be a requirement too.
Making these can wait until we've concluded the conversation on the Serializer
implementation though.
On the subject of using the @Serializable
annotation on Axon components, and if that even works for Java classes, that is.
Kudos, SonarCloud Quality Gate passed!
|
Thank you for the great comments @smcvb. The discussion in https://github.com/AxonFramework/extension-kotlin/pull/124/files#r645048334 remains unresolved which will require more changes whatever solution is chosen. |
|
||
@OptIn(ExperimentalSerializationApi::class) | ||
@Serializer(forClass = ConfigToken::class) | ||
class ConfigTokenSerializer : KSerializer<ConfigToken> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the custom serializer implemented manually for the internal Axon type. These type of implementations are needed if the @Serializable
annotation is not used in combination with the compiler plugin.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd assume this should be constructed for every concrete implementation of serializable Axon objects in that case, correct?
This would at least enable moving further with this serializer, so that's good.
If we go this route, it will require some KDoc, of course ;-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just saw https://github.com/Kotlin/kotlinx.serialization/blob/f673bc2/docs/serializers.md#deriving-external-serializer-for-another-kotlin-class-experimental for the first time. This would allow us to skip the 'manual' implementation of the serializer.
Unfortunately I can't yet get it to work, I run into this bug in the Kotlin complier plugin (Kotlin/kotlinx.serialization#1680).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have you tried Surrogate serializers and Delegating serializers?
They look much simpler and might just get the job done. Hopefully, they might just work and there would be no hidden issues like you encountered previously.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(#124 (comment)) I changed the fully-custom example ConfigTokenSerializer
into a serializer using a ConfigTokenSurrogate
. Its slightly better maintainable, but still a LOT of work to do (and maintain) for every Axon class.
The work means, for every Axon class:
- Create a Kotlin class with
@Serializable
- Put all the fields of the class into the surrogate, and add converter methods to convert from the class to the surrogate, and back.
- Manually implement a serializer for that class, using the SerialDescriptor generated by the surrogate.
- During serialization, convert from the real value to a surrogate and serialize it.
- During deserialization, deserialize the surrogate and convert from the surrogate to the value of the real class.
The serializers for surrogates are very simple and similar (only a few lines of code). However the surrogates themselves need to contain exactly the public data fields of the class they are a surrogate for. This will cause bugs if a field is forgotten in the surrogate.
import kotlinx.serialization.encoding.encodeStructure | ||
import org.axonframework.eventhandling.tokenstore.ConfigToken | ||
|
||
@OptIn(ExperimentalSerializationApi::class) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would you be able to elaborate a little on the ExperimentalSerializationApi
you're "opting in" here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The @Serializer
annotation below is marked as experimental. It might change its implementation slightly between versions of Kotlin Serialization. (https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx-serialization-core/kotlinx.serialization/-experimental-serialization-api/index.html)
We might wait to merge this feature until the used serialization API is marked fully stable, although the Axon extension-kotlin
library is also marked as experimental itself.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So to clarify, only @Serializer
which is used to hook custom serializer objects is marked as Experimental, the rest of Json serialization is not experimental anymore and is stable. Is this a correct assumption @hiddewie ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes exactly. The following is stable:
@Serializable
class XXX
But the following needs experimental opt in:
@Serializer(forClass = QQQ::class)
class XXX
In particular doing this for Java classes gives compiler errors, and it might simply not be supported (see Kotlin/kotlinx.serialization#1687)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for returning so late here. Times are rather busy, and some things sadly got precedence.
On any note, I think it is paramount we first tackle the Axon Serializable components predicament, specifically on whether that annotation would be an option. From my end, I'll chat with the team if we feel it has a time and place inside the core.
No problem :) I investigated the compilation of the Then I found https://github.com/Kotlin/kotlinx.serialization/blob/f673bc2/docs/serializers.md#deriving-external-serializer-for-another-kotlin-class-experimental, which could mean that we create singleton objects marked as |
FYI, @hiddewie, I've adjusted the milestone to 0.3.0 instead of 0.2.0. The intent for this is the desire to release 0.2.0 of the Kotlin extension. This, by the way, doesn't mean a 0.3.0 release couldn't be around the corner, which might contain this serializer. ;-) |
kotlin/src/main/kotlin/org/axonframework/extensions/kotlin/serialization/KotlinSerializer.kt
Outdated
Show resolved
Hide resolved
// Class<T>: T must be non-null | ||
val kClass = (this as Class<Any>).kotlin | ||
|
||
val companion = kClass.companionObject |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Either way, we can not assume that every class that comes here will have a serializer attached to it (nor be a Kotlin class)
If there is no serializer defined, or just based on types, we can fall back on a registry that we can keep as a map of Type (Class, Class name, etc) and a Serializer. That map could be configurable so someone might add support for classes without @Serializable
like Java Dates, and we can pre-populate it with AF serializers configuration. Or keep the AF serializers in a separate config, but that's a finer detail.
This and
this is what I mean, as we need to pass the serializer explicitly in the very broad generic setting anyway, we might as well keep it somewhere in a config map, similar as to how this cache is doing but is just caching all serializers.
|
||
@OptIn(ExperimentalSerializationApi::class) | ||
@Serializer(forClass = ConfigToken::class) | ||
class ConfigTokenSerializer : KSerializer<ConfigToken> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have you tried Surrogate serializers and Delegating serializers?
They look much simpler and might just get the job done. Hopefully, they might just work and there would be no hidden issues like you encountered previously.
import kotlinx.serialization.encoding.encodeStructure | ||
import org.axonframework.eventhandling.tokenstore.ConfigToken | ||
|
||
@OptIn(ExperimentalSerializationApi::class) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So to clarify, only @Serializer
which is used to hook custom serializer objects is marked as Experimental, the rest of Json serialization is not experimental anymore and is stable. Is this a correct assumption @hiddewie ?
Sorry for the long wait, I didn't forget the PR but it took some time until I could dive into it again! I updated this branch to include all changes in I updated the Kotlin serialization version to 1.3.2, but this makes no changes to everything I have observed so far. In particular I experimented with a Surrogate serializer (#124 (comment)). This works, but still requires a lot of maintenance work to 'wrap' each Axon internal class with a surrogate class in this library, while copying the public fields manually into the surrogate class for serialization. Performance-wise this is also not ideal. I tested the automatically-generated serializer for external classes functionality again, but this still gives compiler errors:
If I manually type the simple class like |
Removing the milestone as this is still under discussion, while the release is intended to come soon. Will add this to the following release milestone for further discussions. |
Marking this pull request as obsolete through the introduction of #338. |
(Followup from #121 which had to be closed because I deleted my Github fork.)
This PR adds support for Kotlin serialization, see issue #13.
This PR includes #120 (this is required in order to use the stable version of
kotlin.serialization
).In summary:
KotlinSerializer
which implements AxonSerializer
.RevisionResolver
,Converter
, andJson
, with the same defaults as the Jackson serializer implementation.Json
is configurable, applications can define how JSON should be serialized based on business requirements.kotlinSerializer
to create aKotlinSerializer
with configuration options..serializer()
method cannot be found on a companion object of a to-serialize object.Class -> KSerializer
is cached for performance and to avoid too much reflection.Ref https://kotlinlang.org/docs/serialization.html with basic overview.
Ref https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/serialization-guide.md with serialization guide.