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 Instants 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
TYPEwas 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 :)