[JAVA-5296] Can't use Kotlinx DateTime with Kotlin MongoDB Driver Created: 20/Jan/24  Updated: 25/Jan/24  Resolved: 25/Jan/24

Status: Closed
Project: Java Driver
Component/s: None
Affects Version/s: 4.11.0
Fix Version/s: None

Type: Bug Priority: Trivial - P5
Reporter: Ahmed Hnewa Assignee: Ross Lawley
Resolution: Works as Designed Votes: 0
Labels: bug, kotlin, question
Remaining Estimate: Not Specified
Time Spent: Not Specified
Original Estimate: Not Specified

Issue Links:
Related
Documentation Changes Summary:

1. What would you like to communicate to the user about this feature?
2. Would you like the user to see examples of the syntax and/or executable code and its output?
3. Which versions of the driver/connector does this apply to?


 Description   

So KMongo for Kotlin is deprecated because of the new official driver (which has been around since 2016)
 
I'm trying to insert data to the database using an instance of data class that has Instant and LocalDateTime in it
 

@Serializable
data class User(
    val email: String,
    val password: String,
    val isEmailVerified: Boolean,
    val isAccountActivated: Boolean,
    val role: UserRole,
    val pictureUrl: String = "",
    val emailVerification: TokenVerification,
    val forgotPasswordVerification: TokenVerification,
    val createdAt: Instant,
    val updatedAt: Instant,
    val data: UserData,
)

 
But I'm getting the following exception:
 

org.bson.codecs.configuration.CodecConfigurationException: Can't find a codec for CodecCacheKey\{clazz=class kotlinx.datetime.Instant, types=null}.
at org.bson.internal.ProvidersCodecRegistry.lambda$get$0(ProvidersCodecRegistry.java:87)
at java.base/java.util.Optional.orElseGet(Optional.java:364)
at org.bson.internal.ProvidersCodecRegistry.get(ProvidersCodecRegistry.java:80)
at org.bson.internal.ChildCodecRegistry.get(ChildCodecRegistry.java:68)
at org.bson.codecs.kotlin.DataClassCodec$Companion.getCodec(DataClassCodec.kt:226)
at org.bson.codecs.kotlin.DataClassCodec$Companion.getCodec(DataClassCodec.kt:199)
at org.bson.codecs.kotlin.DataClassCodec$Companion.create$bson_kotlin(DataClassCodec.kt:148)
at org.bson.codecs.kotlin.DataClassCodecProvider.get(DataClassCodecProvider.kt:28)
at org.bson.internal.ProvidersCodecRegistry.getFromCodecProvider(ProvidersCodecRegistry.java:95)
at com.mongodb.KotlinCodecProvider.get(KotlinCodecProvider.java:83)
at org.bson.internal.ProvidersCodecRegistry.getFromCodecProvider(ProvidersCodecRegistry.java:95)
at org.bson.internal.ProvidersCodecRegistry.get(ProvidersCodecRegistry.java:70)
at org.bson.internal.ProvidersCodecRegistry.getFromCodecProvider(ProvidersCodecRegistry.java:95)
at org.bson.codecs.configuration.OverridableUuidRepresentationCodecProvider.get(OverridableUuidRepresentationCodecProvider.java:47)
at org.bson.internal.ProvidersCodecRegistry.getFromCodecProvider(ProvidersCodecRegistry.java:95)
at org.bson.internal.ProvidersCodecRegistry.lambda$get$0(ProvidersCodecRegistry.java:82)
at java.base/java.util.Optional.orElseGet(Optional.java:364)
at org.bson.internal.ProvidersCodecRegistry.get(ProvidersCodecRegistry.java:80)
at org.bson.internal.ChildCodecRegistry.get(ChildCodecRegistry.java:68)
at org.bson.codecs.kotlin.DataClassCodec$Companion.getCodec(DataClassCodec.kt:226)
at org.bson.codecs.kotlin.DataClassCodec$Companion.getCodec(DataClassCodec.kt:199)
at org.bson.codecs.kotlin.DataClassCodec$Companion.create$bson_kotlin(DataClassCodec.kt:148)
at org.bson.codecs.kotlin.DataClassCodecProvider.get(DataClassCodecProvider.kt:28)
at org.bson.internal.ProvidersCodecRegistry.getFromCodecProvider(ProvidersCodecRegistry.java:95)
at com.mongodb.KotlinCodecProvider.get(KotlinCodecProvider.java:83)
at org.bson.internal.ProvidersCodecRegistry.getFromCodecProvider(ProvidersCodecRegistry.java:95)
at org.bson.internal.ProvidersCodecRegistry.get(ProvidersCodecRegistry.java:70)
at org.bson.internal.ProvidersCodecRegistry.getFromCodecProvider(ProvidersCodecRegistry.java:95)
at org.bson.codecs.configuration.OverridableUuidRepresentationCodecProvider.get(OverridableUuidRepresentationCodecProvider.java:47)
at org.bson.internal.ProvidersCodecRegistry.getFromCodecProvider(ProvidersCodecRegistry.java:95)
at org.bson.internal.ProvidersCodecRegistry.lambda$get$0(ProvidersCodecRegistry.java:82)
at java.base/java.util.Optional.orElseGet(Optional.java:364)
at org.bson.internal.ProvidersCodecRegistry.get(ProvidersCodecRegistry.java:80)
at org.bson.internal.ProvidersCodecRegistry.get(ProvidersCodecRegistry.java:50)
at com.mongodb.internal.operation.Operations.getCodec(Operations.java:746)
at com.mongodb.internal.operation.Operations.bulkWrite(Operations.java:466)
at com.mongodb.internal.operation.Operations.insertOne(Operations.java:392)
at com.mongodb.internal.operation.AsyncOperations.insertOne(AsyncOperations.java:202)
at com.mongodb.reactivestreams.client.internal.MongoOperationPublisher.lambda$insertOne$6(MongoOperationPublisher.java:261)
at com.mongodb.reactivestreams.client.internal.MongoOperationPublisher.createWriteOperationMono(MongoOperationPublisher.java:446)
at com.mongodb.reactivestreams.client.internal.MongoOperationPublisher.createSingleWriteRequestMono(MongoOperationPublisher.java:454)
at com.mongodb.reactivestreams.client.internal.MongoOperationPublisher.insertOne(MongoOperationPublisher.java:261)
at com.mongodb.reactivestreams.client.internal.MongoCollectionImpl.insertOne(MongoCollectionImpl.java:367)
at com.mongodb.kotlin.client.coroutine.MongoCollection.insertOne(MongoCollection.kt:627)
at com.mongodb.kotlin.client.coroutine.MongoCollection.insertOne$default(MongoCollection.kt:626)

 
I don't know how to create a codec for Kotlinx DateTime types and I can't find one, so can anyone tell me how to do it? thank you for your time and efforts.
 
the official docs says everything that is Serializable can convert to BSON format in efficient way. is there any chance I'm wrong?



 Comments   
Comment by Ross Lawley [ 25/Jan/24 ]

Closing as "works as designed" - I have opened a documentation ticket to improve the documentation for this.

Many Thanks,

Ross

Comment by Ross Lawley [ 25/Jan/24 ]

Hi ahmed4496.hnewa@gmail.com,

This is because the Instant class uses the InstantIso8601Serializer by default. So to use an alternative it must be set by the data class.

Ross

Comment by Ahmed Hnewa [ 25/Jan/24 ]

It's like you said, but it would be helpful to add another library that support the kotlinx date time

Comment by Ahmed Hnewa [ 25/Jan/24 ]

The problem is that kotlinx bson is not storing Instant as Date instead it store it as a String

Comment by Ahmed Hnewa [ 25/Jan/24 ]

While the error is now gone but I have another error:

@Serializable
data class TokenVerification(
val token: String,
val expiresAt: Instant
)

Caused by: org.bson.BsonInvalidOperationException: Reading field 'expiresAt' failed expected STRING type but found: DATE_TIME.
at org.bson.codecs.kotlinx.DefaultBsonDecoder.decodeString(BsonDecoder.kt:302)
at kotlinx.datetime.serializers.InstantIso8601Serializer.deserialize(InstantSerializers.kt:27)
at kotlinx.datetime.serializers.InstantIso8601Serializer.deserialize(InstantSerializers.kt:21)
at kotlinx.serialization.encoding.Decoder$DefaultImpls.decodeSerializableValue(Decoding.kt:257)
at kotlinx.serialization.encoding.AbstractDecoder.decodeSerializableValue(AbstractDecoder.kt:16)
at kotlinx.serialization.encoding.AbstractDecoder.decodeSerializableValue(AbstractDecoder.kt:43)

Comment by Ross Lawley [ 25/Jan/24 ]

Hi ahmed4496.hnewa@gmail.com,

By using bson kotlinx, we only have to add the dependency? The docs didn't cover how to register it, it only covers how to customize it

Thats correct, the driver will detect if its available and automatically register it.

Ross

Comment by Ahmed Hnewa [ 24/Jan/24 ]

Hi, thank you for the response!! By using bson kotlinx, we only have to add the dependency? The docs didn't cover how to register it, it only covers how to customize it

Comment by Ross Lawley [ 24/Jan/24 ]

Hi ahmed4496.hnewa@gmail.com,

Thanks for the ticket. To support kotlinx.serialization you will need to add the bson-kotlinx dependency to your project. See the Serialization section of the documentation.

Alternatively, you will need to add a custom codec to the registry to handle kotlinx.datetime.Instant.

The example code I ran (using bson-koltinx) was:

package example
 
import com.mongodb.kotlin.client.MongoClient
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
import org.bson.Document
 
@Serializable
data class Foo(val name: String, val createdAt: Instant)
 
fun main() {
 
    val mongoClient = MongoClient.create("mongodb://localhost/")
    val db = mongoClient.getDatabase("test")
 
    val collection = db.getCollection<Foo>("test")
    collection.drop()
    val foo = Foo("Ross",  Clock.System.now())
 
    println(collection.insertOne(foo))
    println(collection.find().first())
    println(collection.withDocumentClass<Document>().find().first().toJson())
 
    db.drop()
    mongoClient.close()
}

Produces:

AcknowledgedInsertOneResult{insertedId=BsonObjectId{value=65b0f2999e693e4c624cd21f}}
Foo(name=Ross, createdAt=2024-01-24T11:20:56.970510Z)
{"_id": {"$oid": "65b0f2999e693e4c624cd21f"}, "name": "Ross", "createdAt": "2024-01-24T11:20:56.970510Z"}

Notice Kotlinx serialization defaults the serialization to a String and not to a BsonDateTime!

To convert an Instant to a BsonDateTime you will need to add your own KSerializer like so:

object InstantAsBsonDateTime : KSerializer<Instant> {
    override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("InstantAsBsonDateTime", PrimitiveKind.STRING)
 
    override fun serialize(encoder: Encoder, value: Instant) {
        when (encoder) {
            is BsonEncoder -> encoder.encodeBsonValue(BsonDateTime(value.toEpochMilliseconds()))
            else -> throw SerializationException("Instant is not supported by ${encoder::class}")
        }
    }
 
    override fun deserialize(decoder: Decoder): Instant {
        return when (decoder) {
            is BsonDecoder -> Instant.fromEpochMilliseconds(decoder.decodeBsonValue().asDateTime().value)
            else -> throw SerializationException("Instant is not supported by ${decoder::class}")
        }
    }
}

Notice we can use the BsonEncoder to handle the conversion of BsonDateTime values.

Update the dataclass to use the custom serializer:

@Serializable
data class Foo(val name: String,
               @Serializable(with = InstantAsBsonDateTime::class)
               val createdAt: Instant)

And then the output is:

AcknowledgedInsertOneResult{insertedId=BsonObjectId{value=65b103372c9a216a2e64c736}}
Foo(name=Ross, createdAt=2024-01-24T12:31:51.745Z)
{"_id": {"$oid": "65b103372c9a216a2e64c736"}, "name": "Ross", "createdAt": {"$date": "2024-01-24T12:31:51.745Z"}}

You can see from the extended Json that createdAt is now a Bson DateTime and not a String.

I hope that helps,

Ross

Comment by Ahmed Hnewa [ 21/Jan/24 ]

I found a workaround this and published it on https://stackoverflow.com/a/77852080/18519412, but still we should be able to use the classes that is Serializable in bson format as the docs said that

Comment by PM Bot [ 20/Jan/24 ]

Hi ahmed4496.hnewa@gmail.com, thank you for reporting this issue! The team will look into it and get back to you soon.

Generated at Thu Feb 08 09:04:13 UTC 2024 using Jira 9.7.1#970001-sha1:2222b88b221c4928ef0de3161136cc90c8356a66.