kotlinx.serialization
kotlinx.serialization copied to clipboard
Annotating property type with @Serializable does not override default serialzer
Describe the bug
The default serializer of a serializable class can be overwritten by annotating the property that should be serialized with SomeOtherSerializer
with @Serializable(with = SomeOtherSerializer::class)
.
Since the Serializable
annotation has the targets PROPERTY
, CLASS
and TYPE
, I expected that this should also work when annotating the type of the property instead of the property itself.
However it seems like this is only true for classes that are not serializable by default and always require specifying a serializer.
The reason I want to annotate the type instead of the property is for using typealiases with it. The project that I want to use this for needs to serialize Instant
s in different formats and to reduce repetition and improve readability I wanted to replace
@Serializable
data class Data(
@Serializable(with = InstantInEpochSecondsSerializer::class)
val instant: Instant
)
with
typealias InstantInEpochSeconds = @Serializable(with = InstantInEpochSecondsSerializer::class) Instant
@Serializable
data class Data(val instant: InstantInEpochSeconds)
To Reproduce
The issue can be reproduced by the following code (requiring kotlinx-datetime as a dependency for the Instant
class)
// the Instant class is annotated with @Serializable(with = InstantIso8601Serializer::class)
@Serializable
data class InstantDefault(val instant: Instant)
@Serializable
data class InstantProperty(
@Serializable(with = InstantInEpochSecondsSerializer::class)
val instant: Instant,
)
@Serializable
data class InstantType(
val instant: @Serializable(with = InstantInEpochSecondsSerializer::class) Instant,
)
// the Duration class is not annotated with @Serializable
@Serializable
data class DurationProperty(
@Serializable(with = DurationInSecondsSerializer::class)
val duration: Duration,
)
@Serializable
data class DurationType(
val duration: @Serializable(with = DurationInSecondsSerializer::class) Duration,
)
inline fun <reified T> printJson(value: T) = println(Json.encodeToString(value))
fun main() {
val instant = Instant.fromEpochSeconds(10)
val duration = 10.seconds
// expected {"instant":"1970-01-01T00:00:10Z"}, got {"instant":"1970-01-01T00:00:10Z"}
printJson(InstantDefault(instant))
// expected {"instant":10}, got {"instant":10}
printJson(InstantProperty(instant))
// this is wrong:
// expected {"instant":10}, got {"instant":"1970-01-01T00:00:10Z"}
printJson(InstantType(instant))
// expected {"duration":10}, got {"duration":10}
printJson(DurationProperty(duration))
// expected {"duration":10}, got {"duration":10}
printJson(DurationType(duration))
}
custom serializers
object InstantInEpochSecondsSerializer : KSerializer<Instant> {
override val descriptor = PrimitiveSerialDescriptor("InstantInEpochSeconds", PrimitiveKind.LONG)
override fun serialize(encoder: Encoder, value: Instant) {
encoder.encodeLong(value.epochSeconds)
}
override fun deserialize(decoder: Decoder): Instant {
return Instant.fromEpochSeconds(decoder.decodeLong())
}
}
object DurationInSecondsSerializer : KSerializer<Duration> {
override val descriptor = PrimitiveSerialDescriptor("DurationInSeconds", PrimitiveKind.LONG)
override fun serialize(encoder: Encoder, value: Duration) {
encoder.encodeLong(value.inWholeSeconds)
}
override fun deserialize(decoder: Decoder): Duration {
return decoder.decodeLong().toDuration(DurationUnit.SECONDS)
}
}
Expected behavior
Instead of printing
{"instant":"1970-01-01T00:00:10Z"}
{"instant":10}
{"instant":"1970-01-01T00:00:10Z"}
{"duration":10}
{"duration":10}
it should print
{"instant":"1970-01-01T00:00:10Z"}
{"instant":10}
{"instant":10}
{"duration":10}
{"duration":10}
(it should use InstantInEpochSecondsSerializer
instead of InstantIso8601Serializer
for the InstantType.instant
property)
Environment
- Kotlin version: reproduced on 1.6.10 and 1.6.20
- Library version: 1.3.2
- Kotlin platforms: JVM
- Gradle version: reproduced on 7.4 and 7.4.2
Target TYPE
was added to be able to specify serializers inside other types, e.g. List<@Serializable(with = InstantInEpochSecondsSerializer::class) Instant>
But I agree it should also work in a described way. However, probably not with typealias
.
Target
TYPE
was added to be able to specify serializers inside other types, e.g.List<@Serializable(with = InstantInEpochSecondsSerializer::class) Instant>
This indeed works as intended.
But I agree it should also work in a described way. However, probably not with
typealias
.
Since the typealias
approach works for Duration
and inside other types like List
, I don't see why it wouldn't work everywhere once this bug is fixed.
I further found out that this seems to be only the case for serializable classes that use a custom serializer and not the generated one.
If Instant
in the above example is replaced by
@Serializable(with = SomeCustomSerializer::class)
data class SomeClass(val data: Int)
and InstantInEpochSecondsSerializer
by OverrideSerializer
I can still observe this bug, however if Instant
is instead replaced by
@Serializable
data class SomeClass(val data: Int)
I can no longer observe this bug.
custom serializers
object SomeCustomSerializer : KSerializer<SomeClass> {
override val descriptor = PrimitiveSerialDescriptor("SomeClass", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: SomeClass) {
encoder.encodeString(value.data.toString())
}
override fun deserialize(decoder: Decoder): SomeClass {
return SomeClass(decoder.decodeString().toInt())
}
}
object OverrideSerializer : KSerializer<SomeClass> {
override val descriptor = PrimitiveSerialDescriptor("SomeClassAsAlwaysTheSame", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: SomeClass) {
encoder.encodeString("AlwaysTheSame");
}
override fun deserialize(decoder: Decoder): SomeClass {
decoder.decodeString()
return SomeClass(1)
}
}
Do you think I could contribute a fix myself? I haven't worked with kotlin compiler/plugin source before, so where would be the right place to get started?
Plugin sources are located in the Kotlin repository: https://github.com/JetBrains/kotlin/tree/master/plugins/kotlin-serialization/kotlin-serialization-compiler (ignore the README though, it's outdated since all Kotlin idea plugins are now in intellij monorepo).
It may be hard to get a grasp on large compiler API, but of course, nothing is impossible :)
It seems like you did some work related to this issue @sandwwraith. Will this be fixed now in some next version?
@Lukellmann Yes, the fix should go to the master
branch of Kotlin which is now 1.8.20. I'll see if it's possible to backport fix to 1.8.0
That's amazing, thanks :)