Persist/Retrieve using a Data Class with UUID keys

Hi,

We have a Data Class something like this:

data class MyClass(val name: String, val attributes: Map<UUID, String>)

Using the Kotlin Coroutine drivers, we’re unable to retrieve data from our collection. The data is stored ok - UUIDs are strings in the DB - but it blows up when we try to retrieve the data.

I understand that keys have to be strings, but is there someway we can intercept the deserialisation to grab the string and convert it into a UUID so that the Map works?

We’ve been using json/gson code and the standard Java library up until now, but that’s hugely memory inefficient and we were hoping the Kotlin driver and kotlinx serialisation would help!

We’ve tried various codecs but it seems like the code throws an exception as soon as it realises there are non-string keys in the Map object.

Any help appreciated.

Thanks,
-Simon.

Hi @Simon_Burgess,

This is a little tricky to achieve, essentially you’d need a CodecProvider that can handle Map<UUID, String> and that needs to be higher in the registry than the default MapCodec implementation.

Here is one such example:

package example

import com.mongodb.MongoClientSettings
import org.bson.BsonReader
import org.bson.BsonWriter
import org.bson.Document
import org.bson.codecs.Codec
import org.bson.codecs.DecoderContext
import org.bson.codecs.EncoderContext
import org.bson.codecs.configuration.CodecProvider
import org.bson.codecs.configuration.CodecRegistries.fromProviders
import org.bson.codecs.configuration.CodecRegistry
import java.lang.reflect.Type
import java.util.UUID

class MyClassMapCodecProvider : CodecProvider {
    // No type args - fall through
    override fun <T : Any?> get(clazz: Class<T>, registry: CodecRegistry): Codec<T>? = null

    @Suppress("UNCHECKED_CAST")
    override fun <T : Any?> get(clazz: Class<T>, typeArguments: MutableList<Type>, registry: CodecRegistry): Codec<T>? {
        if (clazz == Map::class.java && typeArguments.equals(listOf(UUID::class.java, String::class.java))) {
            return MyClassMapCodec() as Codec<T>?
        }
        // fall through, not the clazz we're looking for
        return null
    }
}

class MyClassMapCodec: Codec<Map<UUID, String>> {

    // Get a codec for Map<String, String>
    @Suppress("UNCHECKED_CAST")
    private val mapCodec: Codec<Map<String, String>> =
        MongoClientSettings.getDefaultCodecRegistry()
            .get(Map::class.java, listOf(String::class.java, String::class.java)) as Codec<Map<String, String>>

    override fun encode(writer: BsonWriter, value: Map<UUID, String>, encoderContext: EncoderContext) {
        mapCodec.encode(writer, value.mapKeys { kv -> kv.key.toString() }, encoderContext)
    }

    override fun decode(reader: BsonReader, decoderContext: DecoderContext): Map<UUID, String> =
        mapCodec.decode(reader, decoderContext).mapKeys { kv -> UUID.fromString(kv.key) }

    override fun getEncoderClass(): Class<Map<UUID, String>> = emptyMap<UUID, String>().javaClass
}

fun main() {

    data class MyClass(val name: String, val attributes: Map<UUID, String>)

    val client = MongoClient.create()
    client.use {
        val database = client.getDatabase("test")
        database.drop()

        val collection = database.getCollection<Document>("test")
        collection.insertOne(Document()
            .append("name", "a")
            .append("attributes", mapOf(UUID.randomUUID().toString() to "b")))
        println(collection.find().toCollection(ArrayList()))


        // Can we retrieve it?
        val myClassCollection = database.getCollection<MyClass>("test")
            .withCodecRegistry(fromProviders(MyClassMapCodecProvider(), MongoClientSettings.getDefaultCodecRegistry()))
        println(myClassCollection.find().toCollection(ArrayList()))

        // Can we add a new one?
        myClassCollection.insertOne(MyClass("b", mapOf(UUID.randomUUID() to "c")))
        println(myClassCollection.find().toCollection(ArrayList()))
    }
}

I hope that helps,

Ross