BsonInvalidOperationException in ktor

232 Views Asked by At

Being new to MongoDB, I'm currently integrating the kMongo library to my ktor project, and trying to create a database to read & write event models to.

Following the instructions for object mapping in the kMongo user manual, I've created a mongoId field which gets serialised as a String named _id.

My event model is a data class, nested in sealed classes but gets serialised correctly by KotlinX-Serialization. The model looks as such:

sealed class Event {
    @SerialName("_id") abstract val mongoId: String
    abstract val id: ID.Event
    abstract val dateTime: LocalDateTime

    fun asString() = id.toString()

    sealed class Hiring : Event() {
        @SerialName("_id") abstract override val mongoId: String
        abstract override val id: ID.Event
        abstract override val dateTime: LocalDateTime

        @Serializable
        data class Start(
            override val id: ID.Event,
            override val dateTime: LocalDateTime,
            val hiringDetailsId: ID.HiringDetails
        ) : Hiring() {
            @SerialName("_id") override val mongoId: String = id.asString()
        }
        ...

In a repository class, I initialise MongoDB and use the generic, parameter-less find() on a collection to retrieve all Event models from the database:

    ...
    private val kmongo = KMongo.createClient().coroutine.client
    private val db = kmongo.getDatabase("test")
    private val eventCollection = db.getCollection<Event>().coroutine
    ...
    override suspend fun getAllEvents() = eventCollection.find().toList()

Then inside of the Main class, I try to load the Event data on a click trigger:

...
val id = ID.Event(UUID())
...
it.on.click {
   runBlocking {
      val events = eventRepo.getAllEvents().toString()
      logger.debug { events }
   }
}

The strange part starts here, the server starts correctly and MongoDB is initialised correctly, but as soon as I try to do the read on the click trigger, I am presented with following error:

org.bson.BsonInvalidOperationException: readString can only be called when CurrentBSONType is STRING, not when CurrentBSONType is DOCUMENT.
    at org.bson.AbstractBsonReader.verifyBSONType(AbstractBsonReader.java:689)
    at org.bson.AbstractBsonReader.checkPreconditions(AbstractBsonReader.java:721)
    at org.bson.AbstractBsonReader.readString(AbstractBsonReader.java:456)
    at com.github.jershell.kbson.FlexibleDecoder.decodeString(BsonFlexibleDecoder.kt:130)
    at kotlinx.serialization.encoding.AbstractDecoder.decodeStringElement(AbstractDecoder.kt:58)
    at kotlinx.serialization.internal.AbstractPolymorphicSerializer.deserialize(AbstractPolymorphicSerializer.kt:52)
    at kotlinx.serialization.encoding.Decoder$DefaultImpls.decodeSerializableValue(Decoding.kt:257)
    at kotlinx.serialization.encoding.AbstractDecoder.decodeSerializableValue(AbstractDecoder.kt:16)
    at org.litote.kmongo.serialization.SerializationCodec.decode(SerializationCodec.kt:66)
    at com.mongodb.internal.operation.CommandResultArrayCodec.decode(CommandResultArrayCodec.java:52)
    at com.mongodb.internal.operation.CommandResultDocumentCodec.readValue(CommandResultDocumentCodec.java:60)
    at org.bson.codecs.BsonDocumentCodec.decode(BsonDocumentCodec.java:87)
    at org.bson.codecs.BsonDocumentCodec.decode(BsonDocumentCodec.java:42)
    at org.bson.internal.LazyCodec.decode(LazyCodec.java:48)
    at org.bson.codecs.BsonDocumentCodec.readValue(BsonDocumentCodec.java:104)
    at com.mongodb.internal.operation.CommandResultDocumentCodec.readValue(CommandResultDocumentCodec.java:63)
    at org.bson.codecs.BsonDocumentCodec.decode(BsonDocumentCodec.java:87)
    at org.bson.codecs.BsonDocumentCodec.decode(BsonDocumentCodec.java:42)
    at com.mongodb.internal.connection.ReplyMessage.<init>(ReplyMessage.java:51)
    at com.mongodb.internal.connection.InternalStreamConnection.getCommandResult(InternalStreamConnection.java:535)
    at com.mongodb.internal.connection.InternalStreamConnection.access$500(InternalStreamConnection.java:86)
    at com.mongodb.internal.connection.InternalStreamConnection$2$1.onResult(InternalStreamConnection.java:520)
    at com.mongodb.internal.connection.InternalStreamConnection$2$1.onResult(InternalStreamConnection.java:498)
    at com.mongodb.internal.connection.InternalStreamConnection$MessageHeaderCallback$MessageCallback.onResult(InternalStreamConnection.java:821)
    at com.mongodb.internal.connection.InternalStreamConnection$MessageHeaderCallback$MessageCallback.onResult(InternalStreamConnection.java:785)
    at com.mongodb.internal.connection.InternalStreamConnection$5.completed(InternalStreamConnection.java:645)
    at com.mongodb.internal.connection.InternalStreamConnection$5.completed(InternalStreamConnection.java:642)
    at com.mongodb.internal.connection.AsynchronousChannelStream$BasicCompletionHandler.completed(AsynchronousChannelStream.java:250)
    at com.mongodb.internal.connection.AsynchronousChannelStream$BasicCompletionHandler.completed(AsynchronousChannelStream.java:233)
    at java.base/sun.nio.ch.Invoker.invokeUnchecked(Invoker.java:129)
    at java.base/sun.nio.ch.Invoker.invokeDirect(Invoker.java:160)
    at java.base/sun.nio.ch.UnixAsynchronousSocketChannelImpl.implRead(UnixAsynchronousSocketChannelImpl.java:573)
    at java.base/sun.nio.ch.AsynchronousSocketChannelImpl.read(AsynchronousSocketChannelImpl.java:276)
    at java.base/sun.nio.ch.AsynchronousSocketChannelImpl.read(AsynchronousSocketChannelImpl.java:297)
    at com.mongodb.internal.connection.AsynchronousSocketChannelStream$AsynchronousSocketChannelAdapter.read(AsynchronousSocketChannelStream.java:144)
    at com.mongodb.internal.connection.AsynchronousChannelStream.readAsync(AsynchronousChannelStream.java:118)
    at com.mongodb.internal.connection.AsynchronousChannelStream.readAsync(AsynchronousChannelStream.java:107)
    at com.mongodb.internal.connection.InternalStreamConnection.readAsync(InternalStreamConnection.java:642)
    at com.mongodb.internal.connection.InternalStreamConnection.access$600(InternalStreamConnection.java:86)
    at com.mongodb.internal.connection.InternalStreamConnection$MessageHeaderCallback.onResult(InternalStreamConnection.java:775)
    at com.mongodb.internal.connection.InternalStreamConnection$MessageHeaderCallback.onResult(InternalStreamConnection.java:760)
    at com.mongodb.internal.connection.InternalStreamConnection$5.completed(InternalStreamConnection.java:645)
    at com.mongodb.internal.connection.InternalStreamConnection$5.completed(InternalStreamConnection.java:642)
    at com.mongodb.internal.connection.AsynchronousChannelStream$BasicCompletionHandler.completed(AsynchronousChannelStream.java:250)
    at com.mongodb.internal.connection.AsynchronousChannelStream$BasicCompletionHandler.completed(AsynchronousChannelStream.java:233)
    at java.base/sun.nio.ch.Invoker.invokeUnchecked(Invoker.java:129)
    at java.base/sun.nio.ch.UnixAsynchronousSocketChannelImpl.finishRead(UnixAsynchronousSocketChannelImpl.java:447)
    at java.base/sun.nio.ch.UnixAsynchronousSocketChannelImpl.finish(UnixAsynchronousSocketChannelImpl.java:195)
    at java.base/sun.nio.ch.UnixAsynchronousSocketChannelImpl.onEvent(UnixAsynchronousSocketChannelImpl.java:217)
    at java.base/sun.nio.ch.KQueuePort$EventHandlerTask.run(KQueuePort.java:312)
    at java.base/sun.nio.ch.AsynchronousChannelGroupImpl$1.run(AsynchronousChannelGroupImpl.java:113)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
    at java.base/java.lang.Thread.run(Thread.java:833)

According to the stacktrace, something seems to go wrong in the BSON filtering part, despite there being none. When I use the MongoDB compass to validate the object inside of the database, I can see that everything is initialised and written perfectly fine:

MongoDB Compass

The normal id field is used in my software internally as an ID.Event object type whilst the _id is used by Mongo internally.

Can someone point me to what the potential issue could be here?

1

There are 1 best solutions below

0
On

I'm not familiar with Kotlin, but I'd like to dive into this a bit further:

If it wouldn't unwrap the second time, the Mongo Compass would likely reveal the _id or id field to contain a bracket { while these are currently mapped as expected (a String for _id and an object for id).

To confirm, the current structure of your document is (eg here in the playground):

{
  _id: "7d51",
  id: {
    id: "7d51"
  },
  hiringDetailsId: {
    id: "8392"
  }
}

We can see that in your screenshot from Compass where the _id field shows the value being the string directly whereas the other two fields show that the values are Objects (that each contain { id: "<string>" } values).

The error is specifically stating that the code is expecting a string but finding a document:

BsonInvalidOperationException: readString can only be called when CurrentBSONType is STRING, not when CurrentBSONType is DOCUMENT.

I can't speak to the internal unpacking, but it really feels to me like the nested id.id (and potentially also hiringDetailsId.id) is the problem here. Even if it isn't directly related, it would seem to be an opportunity to simplify the schema unless there is a compelling reason to introduce that extra level of nesting.