1 / 1
Oct 2024

Hello community,
I want to read a LocalDate which is stzored as BsonDateTime in mongodb.

im using

org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.6.3 org.mongodb:mongodb-driver-sync:4.11.2 org.mongodb:bson-kotlinx:4.11.2

therefore I created a LocalDateSerializer

package mycompany.samples.localdate import com.github.avrokotlin.avro4k.decoder.RecordDecoder import com.github.avrokotlin.avro4k.encoder.RecordEncoder import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.KSerializer import kotlinx.serialization.SerializationException import kotlinx.serialization.descriptors.PrimitiveKind import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.JsonDecoder import kotlinx.serialization.json.JsonEncoder import org.bson.BsonDateTime import org.bson.BsonType.DATE_TIME import org.bson.codecs.kotlinx.BsonDecoder import org.bson.codecs.kotlinx.BsonEncoder import java.time.Instant import java.time.LocalDate import java.time.ZoneOffset.UTC import java.time.format.DateTimeFormatter @OptIn(ExperimentalSerializationApi::class) object LocalDateSerializer : KSerializer<LocalDate> { private val formatter = DateTimeFormatter.ISO_LOCAL_DATE override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(javaClass.name, PrimitiveKind.STRING) override fun serialize(encoder: Encoder, value: LocalDate) { when (encoder) { is JsonEncoder -> { encoder.encodeString(value.format(formatter)) } is RecordEncoder -> { encoder.encodeString(value.format(formatter)) } is BsonEncoder -> { println("value=$value - epochMilli=${value.atStartOfDay(UTC).toInstant().toEpochMilli()}") encoder.encodeBsonValue(BsonDateTime(value.atStartOfDay(UTC).toInstant().toEpochMilli())) } else -> throw IllegalArgumentException("Unsupported encoder type: ${encoder::class}") } } override fun deserialize(decoder: Decoder): LocalDate = when (decoder) { is JsonDecoder -> LocalDate.parse(decoder.decodeString(), formatter) is RecordDecoder -> LocalDate.parse(decoder.decodeString(), formatter) is BsonDecoder -> decodeBsonValue(decoder) else -> throw throw IllegalArgumentException("Unsupported decoder type: ${decoder::class}") } private fun decodeBsonValue(decoder: BsonDecoder): LocalDate { val bsonValue = decoder.decodeBsonValue() println("bsonValue=$bsonValue") return if (bsonValue.bsonType == DATE_TIME) { val decoded = decoder.decodeBsonValue() println("decoded=$decoded") val converted = decoded as BsonDateTime println("converted=$converted") val millis = converted.value println("millis=$millis") Instant.ofEpochMilli(millis).atZone(UTC).toLocalDate() } else { throw SerializationException( "Deserialization of LocalDate from BSON is not supported for ${bsonValue::class.simpleName}" ) } } } ` this is my Document to persist ` package mycompany.samples.localdate import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import java.time.LocalDate @Serializable data class LocalDatesDocument( @SerialName("_id") val id: String, @Serializable(with = LocalDateSerializer::class) val simpleLocaDate: LocalDate ) ` this is my repository `` package mycompany.samples.localdate import com.mongodb.MongoClientSettings import com.mongodb.WriteConcern import com.mongodb.client.MongoCollection import com.mongodb.client.MongoDatabase import kotlinx.serialization.ExperimentalSerializationApi import org.bson.codecs.configuration.CodecRegistries import org.bson.codecs.configuration.CodecRegistry import org.bson.codecs.kotlinx.KotlinSerializerCodec @OptIn(ExperimentalSerializationApi::class) class LocalDatesRepository(database: MongoDatabase) { private val codecRegistry: CodecRegistry = CodecRegistries.fromRegistries( CodecRegistries.fromCodecs( KotlinSerializerCodec.create<LocalDatesDocument>(org.bson.codecs.kotlinx.defaultSerializersModule) ), MongoClientSettings.getDefaultCodecRegistry() ) internal val collection: MongoCollection<LocalDatesDocument> = database .getCollection("localdates", LocalDatesDocument::class.java) .withWriteConcern(WriteConcern.MAJORITY) .withCodecRegistry(codecRegistry) fun findAll(): List<LocalDatesDocument> = collection.find().toList() fun count(): Long = collection.countDocuments() } ` And this is my simple integration test ` import com.mongodb.client.MongoCollection import com.mongodb.client.MongoDatabase import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import java.time.LocalDate @ExtendWith(SyncMongoExtension::class) internal class LocalDatesRepositoryTests(private val database: MongoDatabase) { private val testee = LocalDatesRepository(database) private val collection: MongoCollection<LocalDatesDocument> = testee.collection @AfterEach fun cleanMongoDatabase() { database.drop() } @Test fun `should persist and load LocalDate objects`() { assertThat(testee.count()).isEqualTo(0L) collection.insertOne(LocalDatesDocument("1", LocalDate.of(1970, 1, 2))) assertThat(testee.count()).isEqualTo(1L) val result = testee.findAll() assertThat(result).containsExactlyInAnyOrder( LocalDatesDocument("1", LocalDate.of(1970, 1, 2)) ) } } ` But I always get this exception ` 11:52:11.757 [Test worker] INFO org.mongodb.driver.client - MongoClient with metadata {"driver": {"name": "mongo-java-driver|sync", "version": "4.11.2"}, "os": {"type": "Darwin", "name": "Mac OS X", "architecture": "aarch64", "version": "14.7"}, "platform": "Java/BellSoft/21.0.3+10-LTS"} created with settings MongoClientSettings{readPreference=primary, writeConcern=WriteConcern{w=null, wTimeout=null ms, journal=null}, retryWrites=true, retryReads=true, readConcern=ReadConcern{level=null}, credential=null, transportSettings=null, streamFactoryFactory=null, commandListeners=[], codecRegistry=ProvidersCodecRegistry{codecProviders=[ValueCodecProvider{}, BsonValueCodecProvider{}, DBRefCodecProvider{}, DBObjectCodecProvider{}, DocumentCodecProvider{}, CollectionCodecProvider{}, IterableCodecProvider{}, MapCodecProvider{}, GeoJsonCodecProvider{}, GridFSFileCodecProvider{}, Jsr310CodecProvider{}, JsonObjectCodecProvider{}, BsonCodecProvider{}, EnumCodecProvider{}, com.mongodb.client.model.mql.ExpressionCodecProvider@725cbf49, com.mongodb.Jep395RecordCodecProvider@2d7f28de, com.mongodb.KotlinCodecProvider@74737991]}, loggerSettings=LoggerSettings{maxDocumentLength=1000}, clusterSettings={hosts=[localhost:32801], srvServiceName=mongodb, mode=SINGLE, requiredClusterType=UNKNOWN, requiredReplicaSetName='null', serverSelector='null', clusterListeners='[]', serverSelectionTimeout='30000 ms', localThreshold='15 ms'}, socketSettings=SocketSettings{connectTimeoutMS=10000, readTimeoutMS=0, receiveBufferSize=0, proxySettings=ProxySettings{host=null, port=null, username=null, password=null}}, heartbeatSocketSettings=SocketSettings{connectTimeoutMS=10000, readTimeoutMS=10000, receiveBufferSize=0, proxySettings=ProxySettings{host=null, port=null, username=null, password=null}}, connectionPoolSettings=ConnectionPoolSettings{maxSize=100, minSize=0, maxWaitTimeMS=120000, maxConnectionLifeTimeMS=0, maxConnectionIdleTimeMS=0, maintenanceInitialDelayMS=0, maintenanceFrequencyMS=60000, connectionPoolListeners=[], maxConnecting=2}, serverSettings=ServerSettings{heartbeatFrequencyMS=10000, minHeartbeatFrequencyMS=500, serverListeners='[]', serverMonitorListeners='[]'}, sslSettings=SslSettings{enabled=false, invalidHostNameAllowed=false, context=null}, applicationName='null', compressorList=[], uuidRepresentation=STANDARD, serverApi=null, autoEncryptionSettings=null, dnsClient=null, inetAddressResolver=null, contextProvider=null} 11:52:11.782 [cluster-ClusterId{value='66fd17cb93537324762dab84', description='null'}-localhost:32801] INFO org.mongodb.driver.cluster - Monitor thread successfully connected to server with description ServerDescription{address=localhost:32801, type=REPLICA_SET_PRIMARY, state=CONNECTED, ok=true, minWireVersion=0, maxWireVersion=21, maxDocumentSize=16777216, logicalSessionTimeoutMinutes=30, roundTripTimeNanos=28720125, setName='docker-rs', canonicalAddress=51d7dadae0fa:27017, hosts=[51d7dadae0fa:27017], passives=[], arbiters=[], primary='51d7dadae0fa:27017', tagSet=TagSet{[]}, electionId=7fffffff0000000000000001, setVersion=1, topologyVersion=TopologyVersion{processId=66fd17c8820bf8d6c767a3bb, counter=6}, lastWriteDate=Wed Oct 02 11:52:11 CEST 2024, lastUpdateTimeNanos=17160271895166} value=1970-01-02 - epochMilli=86400000 bsonValue=BsonDateTime{value=86400000} readDateTime can only be called when State is VALUE, not when State is END_OF_DOCUMENT. org.bson.BsonInvalidOperationException: readDateTime can only be called when State is VALUE, not when State is END_OF_DOCUMENT. at org.bson.AbstractBsonReader.throwInvalidState(AbstractBsonReader.java:668) at org.bson.AbstractBsonReader.verifyBSONType(AbstractBsonReader.java:686) at org.bson.AbstractBsonReader.checkPreconditions(AbstractBsonReader.java:721) at org.bson.AbstractBsonReader.readDateTime(AbstractBsonReader.java:309) at org.bson.codecs.BsonDateTimeCodec.decode(BsonDateTimeCodec.java:31) at org.bson.codecs.BsonDateTimeCodec.decode(BsonDateTimeCodec.java:28) at org.bson.codecs.BsonValueCodec.decode(BsonValueCodec.java:55) at org.bson.codecs.kotlinx.DefaultBsonDecoder.decodeBsonValue(BsonDecoder.kt:180) at mycompany.samples.localdate.LocalDateSerializer.decodeBsonValue(LocalDateSerializer.kt:57) at mycompany.samples.localdate.LocalDateSerializer.deserialize(LocalDateSerializer.kt:49) at mycompany.samples.localdate.LocalDateSerializer.deserialize(LocalDateSerializer.kt:24) 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) at kotlinx.serialization.encoding.AbstractDecoder.decodeSerializableElement(AbstractDecoder.kt:70) at mycompany.samples.localdate.LocalDatesDocument$$serializer.deserialize(LocalDatesDocument.kt:7) at mycompany.samples.localdate.LocalDatesDocument$$serializer.deserialize(LocalDatesDocument.kt:7) at org.bson.codecs.kotlinx.KotlinSerializerCodec.decode(KotlinSerializerCodec.kt:182) 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:53) 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:48) at com.mongodb.internal.connection.InternalStreamConnection.getCommandResult(InternalStreamConnection.java:567) at com.mongodb.internal.connection.InternalStreamConnection.receiveCommandMessageResponse(InternalStreamConnection.java:461) at com.mongodb.internal.connection.InternalStreamConnection.sendAndReceive(InternalStreamConnection.java:372) at com.mongodb.internal.connection.UsageTrackingInternalConnection.sendAndReceive(UsageTrackingInternalConnection.java:114) at com.mongodb.internal.connection.DefaultConnectionPool$PooledConnection.sendAndReceive(DefaultConnectionPool.java:765) at com.mongodb.internal.connection.CommandProtocolImpl.execute(CommandProtocolImpl.java:76) at com.mongodb.internal.connection.DefaultServer$DefaultServerProtocolExecutor.execute(DefaultServer.java:209) at com.mongodb.internal.connection.DefaultServerConnection.executeProtocol(DefaultServerConnection.java:115) at com.mongodb.internal.connection.DefaultServerConnection.command(DefaultServerConnection.java:83) at com.mongodb.internal.connection.DefaultServerConnection.command(DefaultServerConnection.java:74) at com.mongodb.internal.connection.DefaultServer$OperationCountTrackingConnection.command(DefaultServer.java:299) at com.mongodb.internal.operation.SyncOperationHelper.createReadCommandAndExecute(SyncOperationHelper.java:273) at com.mongodb.internal.operation.FindOperation.lambda$execute$1(FindOperation.java:325) at com.mongodb.internal.operation.SyncOperationHelper.lambda$withSourceAndConnection$0(SyncOperationHelper.java:127) at com.mongodb.internal.operation.SyncOperationHelper.withSuppliedResource(SyncOperationHelper.java:152) at com.mongodb.internal.operation.SyncOperationHelper.lambda$withSourceAndConnection$1(SyncOperationHelper.java:126) at com.mongodb.internal.operation.SyncOperationHelper.withSuppliedResource(SyncOperationHelper.java:152) at com.mongodb.internal.operation.SyncOperationHelper.withSourceAndConnection(SyncOperationHelper.java:125) at com.mongodb.internal.operation.FindOperation.lambda$execute$2(FindOperation.java:322) at com.mongodb.internal.operation.SyncOperationHelper.lambda$decorateReadWithRetries$12(SyncOperationHelper.java:292) at com.mongodb.internal.async.function.RetryingSyncSupplier.get(RetryingSyncSupplier.java:67) at com.mongodb.internal.operation.FindOperation.execute(FindOperation.java:333) at com.mongodb.internal.operation.FindOperation.execute(FindOperation.java:73) at com.mongodb.client.internal.MongoClientDelegate$DelegateOperationExecutor.execute(MongoClientDelegate.java:153) at com.mongodb.client.internal.MongoIterableImpl.execute(MongoIterableImpl.java:130) at com.mongodb.client.internal.MongoIterableImpl.iterator(MongoIterableImpl.java:90) at com.mongodb.client.internal.MongoIterableImpl.iterator(MongoIterableImpl.java:37) at kotlin.collections.CollectionsKt___CollectionsKt.toCollection(_Collections.kt:1295) at kotlin.collections.CollectionsKt___CollectionsKt.toMutableList(_Collections.kt:1328) at kotlin.collections.CollectionsKt___CollectionsKt.toList(_Collections.kt:1319) at mycompany.samples.localdate.LocalDatesRepository.findAll(LocalDatesRepository.kt:27) at mycompany.samples.localdate.LocalDatesRepositoryTests.should persist and load LocalDate objects(LocalDatesRepositoryTests.kt:29) at java.base/java.lang.reflect.Method.invoke(Method.java:580) at java.base/java.util.ArrayList.forEach(ArrayList.java:1596) at java.base/java.util.ArrayList.forEach(ArrayList.java:1596) ` I have no clue, what is wrong with my LocalDateSerializer. Can you help me please?
read 4 min