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?