Hello everyone,
I am trying to migrate from KMongo to the latest version of the Mongo Kotlin driver using kotinx serialization and I am experiencing an issue with serializers and codecs.
I have this class with a generic parameter:
@Serializable(with = CharacterSerializer::class)
data class Character<P : Any> (
@SerialName("_id") val id: String,
val name: String,
val player: P,
val race: String?,
val territory: String?,
@SerialName("class") val characterClass: List<String> = emptyList(),
val status: CharacterStatus = CharacterStatus.active,
val masterMS: Int = 0,
@SerialName("PBCMS") val pbcMS: Int = 0,
val errataMS: Int = 0,
val sessionMS: Int = 0,
val errata: List<Errata> = emptyList(),
@Contextual val created: Date? = null,
@Contextual val lastPlayed: Date? = null,
@Contextual val lastMastered: Date? = null,
val age: Int? = null,
val reputation: Map<String, Int>? = emptyMap(),
val buildings: Map<String, List<Building>> = emptyMap(),
val inventory: Map<String, Int> = emptyMap(),
val languages: Set<ProficiencyStub> = emptySet(),
val money: Float = 0f,
val proficiencies: Set<ProficiencyStub> = emptySet(),
val labels: Set<LabelStub> = emptySet()
)
and I also implemented a serializer with a generic parameter for it:
class CharacterSerializer<P : Any>(
private val playerSerializer: KSerializer<P>
): KSerializer<Character<P>> {
private val errataSerializer = Errata.serializer()
private val proficiencyStubSerializer = ProficiencyStub.serializer()
private val labelStubSerializer = LabelStub.serializer()
private val characterStatusSerializer = CharacterStatusSerializer
private val reputationSerializer = MapSerializer(String.serializer(), Int.serializer())
private val buildingsSerializer = MapSerializer(String.serializer(), ListSerializer(Building.serializer()))
private val inventorySerializer = MapSerializer(String.serializer(), Int.serializer())
override val descriptor = buildClassSerialDescriptor("character") {
element<String>("id")
element<String>("name")
element("player", playerSerializer.descriptor)
element<String?>("race")
element<String?>("territory")
element<List<String>>("class")
element<CharacterStatus>("status")
element<Int>("masterMS")
element<Int>("PBCMS")
element<Int>("errataMS")
element<Int>("sessionMS")
element<List<Errata>>("errata")
element("created", TimestampDateSerializer.descriptor)
element("lastPlayed", TimestampDateSerializer.descriptor)
element("lastMastered", TimestampDateSerializer.descriptor)
element<Int?>("age")
element<Map<String, Int>?>("reputation")
element<Map<String, List<Building>>>("buildings")
element<Map<String, Int>>("inventory")
element<Set<ProficiencyStub>>("languages")
element<Float>("money")
element<Set<ProficiencyStub>>("proficiencies")
element<Set<LabelStub>>("labels")
}
@OptIn(ExperimentalSerializationApi::class)
override fun serialize(encoder: Encoder, value: Character<P>) {
encoder.encodeStructure(descriptor) {
encodeStringElement(descriptor, 0, value.id)
encodeStringElement(descriptor, 1, value.name)
encodeSerializableElement(descriptor, 2, playerSerializer, value.player)
encodeNullableSerializableElement(descriptor, 3, String.serializer(), value.race)
encodeNullableSerializableElement(descriptor, 4, String.serializer(), value.territory)
encodeSerializableElement(descriptor, 5, ListSerializer(String.serializer()), value.characterClass)
encodeSerializableElement(descriptor, 6, characterStatusSerializer, value.status)
encodeIntElement(descriptor, 7, value.masterMS)
encodeIntElement(descriptor, 8, value.pbcMS)
encodeIntElement(descriptor, 9, value.errataMS)
encodeIntElement(descriptor, 10, value.sessionMS)
encodeSerializableElement(descriptor, 11, ListSerializer(errataSerializer), value.errata)
encodeNullableSerializableElement(descriptor, 12, TimestampDateSerializer, value.created)
encodeNullableSerializableElement(descriptor, 13, TimestampDateSerializer, value.lastPlayed)
encodeNullableSerializableElement(descriptor, 14, TimestampDateSerializer, value.lastMastered)
encodeNullableSerializableElement(descriptor, 15, Int.serializer(), value.age)
encodeNullableSerializableElement(descriptor, 16, reputationSerializer, value.reputation)
encodeSerializableElement(descriptor, 17, buildingsSerializer, value.buildings)
encodeSerializableElement(descriptor, 18, inventorySerializer, value.inventory)
encodeSerializableElement(descriptor, 19, SetSerializer(proficiencyStubSerializer), value.languages)
encodeFloatElement(descriptor, 20, value.money)
encodeSerializableElement(descriptor, 21, SetSerializer(proficiencyStubSerializer), value.proficiencies)
encodeSerializableElement(descriptor, 22, SetSerializer(labelStubSerializer), value.labels)
}
}
@OptIn(ExperimentalSerializationApi::class)
override fun deserialize(decoder: Decoder): Character<P> =
decoder.decodeStructure(descriptor) {
var id: String? = null
var name: String? = null
var player: P? = null
var race: String? = null
var territory: String? = null
var classes: List<String>? = null
var status: CharacterStatus? = null
var masterMS: Int? = null
var pbcMS: Int? = null
var errataMS: Int? = null
var sessionMS: Int? = null
var errata: List<Errata>? = null
var created: Date? = null
var lastPlayed: Date? = null
var lastMastered: Date? = null
var age: Int? = null
var reputation: Map<String, Int>? = null
var buildings: Map<String, List<Building>>? = null
var inventory: Map<String, Int>? = null
var languages: Set<ProficiencyStub>? = null
var money: Float? = null
var proficiencies: Set<ProficiencyStub>? = null
var labels: Set<LabelStub>? = null
while (true) {
when (val index = decodeElementIndex(descriptor)) {
0 -> id = decodeStringElement(descriptor, 0)
1 -> name = decodeStringElement(descriptor, 1)
2 -> player = decodeSerializableElement(descriptor, 2, playerSerializer)
3 -> race = decodeNullableSerializableElement(descriptor, 3, String.serializer())
4 -> territory = decodeNullableSerializableElement(descriptor, 4, String.serializer())
5 -> classes =
try {
decodeSerializableElement(descriptor, 5, ListSerializer(String.serializer()))
} catch (e: Exception) {
listOf(decodeStringElement(descriptor, 5))
}
6 -> status = decodeSerializableElement(descriptor, 6, characterStatusSerializer)
7 -> masterMS = decodeIntElement(descriptor, 7)
8 -> pbcMS = decodeIntElement(descriptor, 8)
9 -> errataMS = decodeIntElement(descriptor, 9)
10 -> sessionMS = decodeIntElement(descriptor, 10)
11 -> errata = decodeSerializableElement(descriptor, 11, ListSerializer(errataSerializer))
12 -> created = decodeNullableSerializableElement(descriptor, 12, TimestampDateSerializer)
13 -> lastPlayed = decodeNullableSerializableElement(descriptor, 13, TimestampDateSerializer)
14 -> lastMastered = decodeNullableSerializableElement(descriptor, 14, TimestampDateSerializer)
15 -> age = decodeNullableSerializableElement(descriptor, 15, Int.serializer())
16 -> reputation = decodeNullableSerializableElement(descriptor, 16, reputationSerializer)
17 -> buildings = decodeSerializableElement(descriptor, 17, buildingsSerializer)
18 -> inventory = decodeSerializableElement(descriptor, 18, inventorySerializer)
19 -> languages = decodeSerializableElement(descriptor, 19, SetSerializer(proficiencyStubSerializer))
20 -> money = decodeFloatElement(descriptor, 20)
21 -> proficiencies = decodeSerializableElement(descriptor, 21, SetSerializer(proficiencyStubSerializer))
22 -> labels = decodeSerializableElement(descriptor, 22, SetSerializer(labelStubSerializer))
CompositeDecoder.DECODE_DONE -> break
else -> error("Unexpected index: $index")
}
}
Character(
id = requireNotNull(id) { "Character id cannot be null" },
name = requireNotNull(name) { "Character name cannot be null" },
player = requireNotNull(player) { "Character player cannot be null" },
race = race,
territory = territory,
characterClass = requireNotNull(classes) { "Character classes cannot be null" },
status = requireNotNull(status) { "Character status cannot be null" },
masterMS = requireNotNull(masterMS) { "Character masterMS cannot be null" },
pbcMS = requireNotNull(pbcMS) { "Character pbcMS cannot be null" },
errataMS = requireNotNull(errataMS) { "Character errataMS cannot be null" },
sessionMS = requireNotNull(sessionMS) { "Character sessionMS cannot be null" },
errata = requireNotNull(errata) { "Character errata cannot be null" },
created = created,
lastPlayed = lastPlayed,
lastMastered = lastMastered,
age = age,
reputation = reputation,
buildings = requireNotNull(buildings) { "Character buildings cannot be null" },
inventory = requireNotNull(inventory) { "Character inventory cannot be null" },
languages = requireNotNull(languages) { "Character languages cannot be null" },
money = requireNotNull(money) { "Character money cannot be null" },
proficiencies = requireNotNull(proficiencies) { "Character proficiencies cannot be null" },
labels = requireNotNull(labels) { "Character labels cannot be null" },
)
}
}
Now, when I try to use a simple find on the collection, I get this error:
Could not find codec for player with type Porg.bson.codecs.configuration.CodecConfigurationException: Could not find codec for player with type P
So, following the documentation, I tried to register a codec and add it to the database. However, I cannot manage to do so. If I try:
val characterCodec = KotlinSerializerCodec.create<Character<Any>>(
serializersModule = SerializersModule {
contextual(Date::class, TimestampDateSerializer)
polymorphic(Any::class) {
subclass(Character.serializer(PolymorphicSerializer(Any::class)))
}
}
)
However, this returns null. I also tried to erase the type on the method with no avail.
val characterCodec = KotlinSerializerCodec.create<Character<*>>(
serializersModule = SerializersModule {
contextual(Date::class, TimestampDateSerializer)
polymorphic(Any::class) {
subclass(Character.serializer(PolymorphicSerializer(Any::class)))
}
}
)
Them I tried to specify the serializer explicitly:
val characterCodec = KotlinSerializerCodec.create(
kClass = Character::class,
serializer = Character.serializer(PolymorphicSerializer(Any::class)),
bsonConfiguration = BsonConfiguration(encodeDefaults = false),
serializersModule = SerializersModule {
contextual(Date::class, TimestampDateSerializer)
polymorphic(Any::class) {
subclass(Character.serializer(PolymorphicSerializer(Any::class)))
}
}
)
However, this does not compile.
Does someone know how to solve this problem?