From f7bd5678f7210d71ca5f1aa00cf077b9aa2bcbb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Fri, 8 May 2026 11:51:58 +0200 Subject: [PATCH 01/10] feat(bson-multiplatform): Serialize BsonDocument, BsonArray and BsonValue --- .../src/commonMain/kotlin/BsonArray.kt | 31 +++++ .../src/commonMain/kotlin/BsonDocument.kt | 31 +++++ .../src/commonMain/kotlin/BsonValue.kt | 31 +++++ .../serialization/MultiplatformDecoder.kt | 6 + .../serialization/MultiplatformEncoder.kt | 8 ++ .../SerializeIntoDocumentTypes.kt | 115 ++++++++++++++++++ 6 files changed, 222 insertions(+) create mode 100644 bson-multiplatform/src/commonTest/kotlin/serialization/SerializeIntoDocumentTypes.kt diff --git a/bson-multiplatform/src/commonMain/kotlin/BsonArray.kt b/bson-multiplatform/src/commonMain/kotlin/BsonArray.kt index fdff657a..69235520 100644 --- a/bson-multiplatform/src/commonMain/kotlin/BsonArray.kt +++ b/bson-multiplatform/src/commonMain/kotlin/BsonArray.kt @@ -18,11 +18,18 @@ package opensavvy.ktmongo.bson.multiplatform import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +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.modules.EmptySerializersModule import kotlinx.serialization.serializer import opensavvy.ktmongo.bson.BsonArray import opensavvy.ktmongo.bson.BsonDecodingException import opensavvy.ktmongo.bson.BsonType +import opensavvy.ktmongo.bson.multiplatform.BsonArray.Serializer import opensavvy.ktmongo.bson.multiplatform.impl.read.MultiplatformBsonArrayList import opensavvy.ktmongo.bson.multiplatform.serialization.BsonDecoderTopLevel import opensavvy.ktmongo.dsl.LowLevelApi @@ -59,6 +66,7 @@ import kotlin.reflect.KType * This class is **not thread-safe**. * Although it is not possible to mutate its state, this class uses internal mutation to lazily decode the BSON stream. */ +@Serializable(with = Serializer::class) class BsonArray internal constructor( override val factory: BsonFactory, private val bytesWithHeader: Bytes, @@ -147,4 +155,27 @@ class BsonArray internal constructor( list.toString() // endregion + + /** + * Serializer for the multiplatform [opensavvy.ktmongo.bson.multiplatform.BsonArray] implementation. + * + * This serializer only supports KtMongo's bson-multiplatform serialization. Other formats are not supported. + * + * If support for other formats is important to you, please [comment on the tracking issue](https://gitlab.com/opensavvy/ktmongo/-/work_items/122). + */ + object Serializer : KSerializer { + // Implementation is empty because the bson-multiplatform encoder has a special case for this type + + private const val NAME = "opensavvy.ktmongo.bson.multiplatform.BsonArray" + + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(NAME, PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: opensavvy.ktmongo.bson.multiplatform.BsonArray) { + error("The $NAME type is only serializable with KtMongo's bson-multiplatform serialization. Found encoder: $encoder") + } + + override fun deserialize(decoder: Decoder): opensavvy.ktmongo.bson.multiplatform.BsonArray { + error("The $NAME type is only deserializable with KtMongo's bson-multiplatform serialization. Found decoder: $decoder") + } + } } diff --git a/bson-multiplatform/src/commonMain/kotlin/BsonDocument.kt b/bson-multiplatform/src/commonMain/kotlin/BsonDocument.kt index 21e05468..c6af74b7 100644 --- a/bson-multiplatform/src/commonMain/kotlin/BsonDocument.kt +++ b/bson-multiplatform/src/commonMain/kotlin/BsonDocument.kt @@ -17,10 +17,17 @@ package opensavvy.ktmongo.bson.multiplatform import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +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.modules.EmptySerializersModule import kotlinx.serialization.serializer import opensavvy.ktmongo.bson.BsonDocument import opensavvy.ktmongo.bson.BsonType +import opensavvy.ktmongo.bson.multiplatform.BsonDocument.Serializer import opensavvy.ktmongo.bson.multiplatform.impl.read.MultiplatformBsonDocumentMap import opensavvy.ktmongo.bson.multiplatform.serialization.BsonDecoder import opensavvy.ktmongo.dsl.LowLevelApi @@ -68,6 +75,7 @@ import kotlin.reflect.KType * This class is **not thread-safe**. * Although it is not possible to mutate its state, this class uses internal mutation to lazily decode the BSON stream. */ +@Serializable(with = Serializer::class) class BsonDocument internal constructor( override val factory: BsonFactory, private val bytesWithHeader: Bytes, @@ -172,4 +180,27 @@ class BsonDocument internal constructor( // endregion + /** + * Serializer for the multiplatform [opensavvy.ktmongo.bson.multiplatform.BsonDocument] implementation. + * + * This serializer only supports KtMongo's bson-multiplatform serialization. Other formats are not supported. + * + * If support for other formats is important to you, please [comment on the tracking issue](https://gitlab.com/opensavvy/ktmongo/-/work_items/122). + */ + object Serializer : KSerializer { + // Implementation is empty because the bson-multiplatform encoder has a special case for this type + + private const val NAME = "opensavvy.ktmongo.bson.multiplatform.BsonDocument" + + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(NAME, PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: opensavvy.ktmongo.bson.multiplatform.BsonDocument) { + error("The $NAME type is only serializable with KtMongo's bson-multiplatform serialization. Found encoder: $encoder") + } + + override fun deserialize(decoder: Decoder): opensavvy.ktmongo.bson.multiplatform.BsonDocument { + error("The $NAME type is only deserializable with KtMongo's bson-multiplatform serialization. Found decoder: $decoder") + } + } + } diff --git a/bson-multiplatform/src/commonMain/kotlin/BsonValue.kt b/bson-multiplatform/src/commonMain/kotlin/BsonValue.kt index 8e706e10..6fb1921f 100644 --- a/bson-multiplatform/src/commonMain/kotlin/BsonValue.kt +++ b/bson-multiplatform/src/commonMain/kotlin/BsonValue.kt @@ -17,11 +17,18 @@ package opensavvy.ktmongo.bson.multiplatform import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +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.modules.EmptySerializersModule import kotlinx.serialization.serializer import opensavvy.ktmongo.bson.* import opensavvy.ktmongo.bson.BsonArray import opensavvy.ktmongo.bson.BsonValue +import opensavvy.ktmongo.bson.multiplatform.BsonValue.Serializer import opensavvy.ktmongo.bson.multiplatform.serialization.BsonDecoder import opensavvy.ktmongo.bson.types.ObjectId import opensavvy.ktmongo.bson.types.Timestamp @@ -72,6 +79,7 @@ import kotlin.time.Instant * This class is **not thread-safe**. * Although it is not possible to mutate its state, this class uses internal mutation to lazily decode the BSON stream. */ +@Serializable(with = Serializer::class) class BsonValue internal constructor( override val factory: BsonFactory, override val type: BsonType, @@ -316,4 +324,27 @@ class BsonValue internal constructor( @OptIn(LowLevelApi::class) override fun hashCode(): Int = BsonValue.hashCode(this) + + /** + * Serializer for the multiplatform [opensavvy.ktmongo.bson.multiplatform.BsonValue] implementation. + * + * This serializer only supports KtMongo's bson-multiplatform serialization. Other formats are not supported. + * + * If support for other formats is important to you, please [comment on the tracking issue](https://gitlab.com/opensavvy/ktmongo/-/work_items/122). + */ + object Serializer : KSerializer { + // Implementation is empty because the bson-multiplatform encoder has a special case for this type + + private const val NAME = "opensavvy.ktmongo.bson.multiplatform.BsonValue" + + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(NAME, PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: opensavvy.ktmongo.bson.multiplatform.BsonValue) { + error("The $NAME type is only serializable with KtMongo's bson-multiplatform serialization. Found encoder: $encoder") + } + + override fun deserialize(decoder: Decoder): opensavvy.ktmongo.bson.multiplatform.BsonValue { + error("The $NAME type is only deserializable with KtMongo's bson-multiplatform serialization. Found decoder: $decoder") + } + } } diff --git a/bson-multiplatform/src/commonMain/kotlin/serialization/MultiplatformDecoder.kt b/bson-multiplatform/src/commonMain/kotlin/serialization/MultiplatformDecoder.kt index a50691c4..14144f29 100644 --- a/bson-multiplatform/src/commonMain/kotlin/serialization/MultiplatformDecoder.kt +++ b/bson-multiplatform/src/commonMain/kotlin/serialization/MultiplatformDecoder.kt @@ -146,6 +146,9 @@ internal class BsonDecoder( private val floatVector = FloatVector.serializer().descriptor private val booleanVector = BooleanVector.serializer().descriptor private val byteVector = ByteVector.serializer().descriptor + private val bsonDocument = BsonDocument.serializer().descriptor + private val bsonArray = BsonArray.serializer().descriptor + private val bsonValue = BsonValue.serializer().descriptor override fun decodeSerializableValue(deserializer: DeserializationStrategy): T { @Suppress("UNCHECKED_CAST") return try { @@ -161,6 +164,9 @@ internal class BsonDecoder( } instant -> source.decodeInstant() as T vector, floatVector, booleanVector, byteVector -> Vector.fromBinaryData(source.decodeBinaryData()) as T + bsonDocument -> source.decodeDocument() as T + bsonArray -> source.decodeArray() as T + bsonValue -> source as T // General case: do what the serializer says else -> deserializer.deserialize(this) diff --git a/bson-multiplatform/src/commonMain/kotlin/serialization/MultiplatformEncoder.kt b/bson-multiplatform/src/commonMain/kotlin/serialization/MultiplatformEncoder.kt index 89f3ea76..27bac4a9 100644 --- a/bson-multiplatform/src/commonMain/kotlin/serialization/MultiplatformEncoder.kt +++ b/bson-multiplatform/src/commonMain/kotlin/serialization/MultiplatformEncoder.kt @@ -28,8 +28,10 @@ import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.modules.EmptySerializersModule import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.serializer +import opensavvy.ktmongo.bson.multiplatform.BsonArray import opensavvy.ktmongo.bson.multiplatform.BsonDocument import opensavvy.ktmongo.bson.multiplatform.BsonFactory +import opensavvy.ktmongo.bson.multiplatform.BsonValue import opensavvy.ktmongo.bson.multiplatform.impl.write.CompletableBsonFieldWriter import opensavvy.ktmongo.bson.multiplatform.impl.write.CompletableBsonValueWriter import opensavvy.ktmongo.bson.types.* @@ -141,6 +143,9 @@ internal class BsonEncoder(override val serializersModule: SerializersModule, va private val floatVector = FloatVector.serializer().descriptor private val booleanVector = BooleanVector.serializer().descriptor private val byteVector = ByteVector.serializer().descriptor + private val bsonDocument = BsonDocument.serializer().descriptor + private val bsonArray = BsonArray.serializer().descriptor + private val bsonValue = BsonValue.serializer().descriptor override fun encodeSerializableValue(serializer: SerializationStrategy, value: T) { when (serializer.descriptor) { // Special cases where we provide our own encoder @@ -150,6 +155,9 @@ internal class BsonEncoder(override val serializersModule: SerializersModule, va uuid -> out.writeBinaryData(4u, (value as Uuid).toByteArray()) instant -> out.writeInstant(value as Instant) vector, floatVector, booleanVector, byteVector -> out.writeBinaryData(0x09u, (value as Vector).toBinaryData()) + bsonDocument -> out.pipe((value as BsonDocument).asValue()) + bsonArray -> out.pipe((value as BsonArray).asValue()) + bsonValue -> out.pipe(value as BsonValue) // General case: do what the serializer says else -> serializer.serialize(this, value) diff --git a/bson-multiplatform/src/commonTest/kotlin/serialization/SerializeIntoDocumentTypes.kt b/bson-multiplatform/src/commonTest/kotlin/serialization/SerializeIntoDocumentTypes.kt new file mode 100644 index 00000000..87cd7b98 --- /dev/null +++ b/bson-multiplatform/src/commonTest/kotlin/serialization/SerializeIntoDocumentTypes.kt @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2026, OpenSavvy and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(LowLevelApi::class, ExperimentalBsonDiffApi::class) + +package opensavvy.ktmongo.bson.multiplatform.serialization + +import kotlinx.serialization.Serializable +import opensavvy.ktmongo.bson.ExperimentalBsonDiffApi +import opensavvy.ktmongo.bson.decode +import opensavvy.ktmongo.bson.diff +import opensavvy.ktmongo.bson.encode +import opensavvy.ktmongo.bson.multiplatform.BsonArray +import opensavvy.ktmongo.bson.multiplatform.BsonDocument +import opensavvy.ktmongo.bson.multiplatform.BsonValue +import opensavvy.ktmongo.bson.multiplatform.factory +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.prepared.runner.testballoon.preparedSuite + +@Serializable +private data class NestedDocument( + val data: BsonDocument, +) + +@Serializable +private data class NestedArray( + val data: BsonArray, +) + +@Serializable +private data class NestedValues( + val values: List, +) + +val SerializeIntoDocumentTypes by preparedSuite { + + test("BsonDocument") { + val document = factory().buildDocument { + writeDocument("data") { + writeString("a", "1") + writeInt32("b", 2) + } + } + + val decoded = document.decode() + + check(decoded.data["a"]?.decodeString() == "1") + check(decoded.data["b"]?.decodeInt32() == 2) + + val encoded = factory().encode(decoded) + + check(document == encoded) { "Diff: ${document diff encoded}" } + } + + test("BsonArray") { + val document = factory().buildDocument { + writeArray("data") { + writeString("1") + writeInt32(2) + } + } + + val decoded = document.decode() + + check(decoded.data[0]?.decodeString() == "1") + check(decoded.data[1]?.decodeInt32() == 2) + + val encoded = factory().encode(decoded) + + check(document == encoded) { "Diff: ${document diff encoded}" } + } + + test("BsonValue") { + val document = factory().buildDocument { + writeArray("values") { + writeString("1") + writeInt32(2) + writeDocument { + writeString("a", "1") + writeInt32("b", 2) + } + writeArray { + writeBoolean(true) + writeBoolean(false) + } + } + } + + val decoded = document.decode() + + check(decoded.values[0].decodeString() == "1") + check(decoded.values[1].decodeInt32() == 2) + check(decoded.values[2].decodeDocument()["a"]?.decodeString() == "1") + check(decoded.values[2].decodeDocument()["b"]?.decodeInt32() == 2) + check(decoded.values[3].decodeArray()[0]?.decodeBoolean() == true) + check(decoded.values[3].decodeArray()[1]?.decodeBoolean() == false) + + val encoded = factory().encode(decoded) + + check(document == encoded) { "Diff: ${document diff encoded}" } + } +} -- 2.51.2 From b0adda9c6546ea829060358161255fd1a134f303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Mon, 11 May 2026 18:55:24 +0200 Subject: [PATCH 02/10] feat(bson-official): Serialize BsonDocument, BsonArray and BsonValue --- bson-official/build.gradle.kts | 3 +- bson-official/src/commonMain/kotlin/Bson.kt | 32 +++++ .../src/jvmMain/kotlin/BsonArray.jvm.kt | 33 ++++++ .../src/jvmMain/kotlin/BsonDocument.jvm.kt | 43 ++++++- .../src/jvmMain/kotlin/BsonFactory.jvm.kt | 43 ++++++- .../src/jvmMain/kotlin/BsonValue.jvm.kt | 39 +++++- .../src/jvmMain/kotlin/types/BsonValue.jvm.kt | 43 +++++++ .../jvmTest/kotlin/OfficialBsonFactory.jvm.kt | 4 +- .../kotlin/SerializeIntoDocumentTypes.kt | 112 ++++++++++++++++++ 9 files changed, 344 insertions(+), 8 deletions(-) create mode 100644 bson-official/src/jvmMain/kotlin/types/BsonValue.jvm.kt create mode 100644 bson-official/src/jvmTest/kotlin/SerializeIntoDocumentTypes.kt diff --git a/bson-official/build.gradle.kts b/bson-official/build.gradle.kts index 0ba9d684..27f996fa 100644 --- a/bson-official/build.gradle.kts +++ b/bson-official/build.gradle.kts @@ -28,10 +28,12 @@ kotlin { sourceSets.commonMain.dependencies { api(projects.bson) + implementation(libs.kotlinx.serialization) } sourceSets.jvmMain.dependencies { api(libs.mongodb.bson.jvm) + compileOnly(libs.mongodb.kotlinx.serialization) } sourceSets.commonTest.dependencies { @@ -45,7 +47,6 @@ kotlin { implementation(libs.mongodb.sync.jvm) implementation(libs.mongodb.kotlinx.serialization) implementation(libs.mongodb.kotlin.reflection) - implementation(libs.kotlinx.serialization) } } diff --git a/bson-official/src/commonMain/kotlin/Bson.kt b/bson-official/src/commonMain/kotlin/Bson.kt index 407e66d1..6c729951 100644 --- a/bson-official/src/commonMain/kotlin/Bson.kt +++ b/bson-official/src/commonMain/kotlin/Bson.kt @@ -16,6 +16,8 @@ package opensavvy.ktmongo.bson.official +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable import opensavvy.ktmongo.bson.BsonArray import opensavvy.ktmongo.bson.BsonDocument import opensavvy.ktmongo.bson.BsonFactory @@ -49,6 +51,7 @@ import kotlin.reflect.typeOf * } * ``` */ +@Serializable(with = opensavvy.ktmongo.bson.official.BsonDocument.Serializer::class) expect class BsonDocument : BsonDocument { override val factory: opensavvy.ktmongo.bson.official.BsonFactory @@ -74,6 +77,15 @@ expect class BsonDocument : BsonDocument { override fun component2(): opensavvy.ktmongo.bson.official.BsonValue } + + /** + * Serializer for the official [opensavvy.ktmongo.bson.official.BsonDocument] implementation. + * + * This serializer only supports the official MongoDB library `org.bson:bson-kotlinx`. + * + * If support for other formats is important to you, please [comment on the tracking issue](https://gitlab.com/opensavvy/ktmongo/-/work_items/122). + */ + object Serializer : KSerializer } /** @@ -102,6 +114,7 @@ expect class BsonDocument : BsonDocument { * } * ``` */ +@Serializable(with = opensavvy.ktmongo.bson.official.BsonArray.Serializer::class) expect class BsonArray : BsonArray { override val factory: opensavvy.ktmongo.bson.official.BsonFactory @@ -119,6 +132,15 @@ expect class BsonArray : BsonArray { override fun asSequence(): Sequence override fun withIndex(): Iterable> + + /** + * Serializer for the official [opensavvy.ktmongo.bson.official.BsonArray] implementation. + * + * This serializer only supports the official MongoDB library `org.bson:bson-kotlinx`. + * + * If support for other formats is important to you, please [comment on the tracking issue](https://gitlab.com/opensavvy/ktmongo/-/work_items/122). + */ + object Serializer : KSerializer } /** @@ -151,6 +173,7 @@ expect class BsonArray : BsonArray { * Some BSON types cannot be represented by a single Kotlin type, so multiple methods are provided to decode * their components. For example: [decodeRegularExpressionPattern] and [decodeRegularExpressionOptions]. */ +@Serializable(with = opensavvy.ktmongo.bson.official.BsonValue.Serializer::class) expect class BsonValue : BsonValue { override val factory: opensavvy.ktmongo.bson.official.BsonFactory @@ -158,6 +181,15 @@ expect class BsonValue : BsonValue { override fun decodeDocument(): opensavvy.ktmongo.bson.official.BsonDocument override fun decodeArray(): opensavvy.ktmongo.bson.official.BsonArray + + /** + * Serializer for the official [opensavvy.ktmongo.bson.official.BsonValue] implementation. + * + * This serializer only supports the official MongoDB library `org.bson:bson-kotlinx`. + * + * If support for other formats is important to you, please [comment on the tracking issue](https://gitlab.com/opensavvy/ktmongo/-/work_items/122). + */ + object Serializer : KSerializer } /** diff --git a/bson-official/src/jvmMain/kotlin/BsonArray.jvm.kt b/bson-official/src/jvmMain/kotlin/BsonArray.jvm.kt index 2f76489c..b457eb51 100644 --- a/bson-official/src/jvmMain/kotlin/BsonArray.jvm.kt +++ b/bson-official/src/jvmMain/kotlin/BsonArray.jvm.kt @@ -16,10 +16,21 @@ package opensavvy.ktmongo.bson.official +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +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 opensavvy.ktmongo.bson.BsonArray import opensavvy.ktmongo.bson.BsonDecodingException +import opensavvy.ktmongo.bson.official.BsonArray.Serializer import opensavvy.ktmongo.dsl.LowLevelApi import org.bson.BsonDocument +import org.bson.codecs.kotlinx.BsonDecoder +import org.bson.codecs.kotlinx.BsonEncoder import kotlin.reflect.KClass import kotlin.reflect.KType import org.bson.BsonArray as OfficialBsonArray @@ -29,6 +40,7 @@ import org.bson.BsonArray as OfficialBsonArray * * To create an instance of this class, see [BsonFactory.readArray]. */ +@Serializable(with = Serializer::class) actual class BsonArray internal constructor( val raw: OfficialBsonArray, actual override val factory: BsonFactory, @@ -239,4 +251,25 @@ actual class BsonArray internal constructor( override fun hashCode(): Int = BsonArray.hashCode(this) + @OptIn(ExperimentalSerializationApi::class) + actual object Serializer : KSerializer { + private const val NAME = "opensavvy.ktmongo.bson.official.BsonArray" + + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor(NAME, PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: opensavvy.ktmongo.bson.official.BsonArray) { + require(encoder is BsonEncoder) { "${this::class} only supports org.bson:bson-kotlinx. See its documentation for details. Found encoder: $encoder" } + + encoder.encodeBsonValue(value.raw) + } + + @OptIn(LowLevelApi::class) + override fun deserialize(decoder: Decoder): opensavvy.ktmongo.bson.official.BsonArray { + require(decoder is BsonDecoder) { "${this::class} only supports org.bson:bson-kotlinx. See its documentation for details. Found decoder: $decoder" } + + val decoded = decoder.decodeBsonValue() + return BsonArray(decoded as OfficialBsonArray, BsonFactory.current()) + } + } } diff --git a/bson-official/src/jvmMain/kotlin/BsonDocument.jvm.kt b/bson-official/src/jvmMain/kotlin/BsonDocument.jvm.kt index 553892fa..c390b8be 100644 --- a/bson-official/src/jvmMain/kotlin/BsonDocument.jvm.kt +++ b/bson-official/src/jvmMain/kotlin/BsonDocument.jvm.kt @@ -16,12 +16,22 @@ package opensavvy.ktmongo.bson.official +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +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 opensavvy.ktmongo.bson.BsonDecodingException import opensavvy.ktmongo.dsl.LowLevelApi import org.bson.BsonBinaryWriter import org.bson.codecs.DecoderContext import org.bson.codecs.DocumentCodec import org.bson.codecs.EncoderContext +import org.bson.codecs.kotlinx.BsonDecoder +import org.bson.codecs.kotlinx.BsonEncoder import org.bson.io.BasicOutputBuffer import kotlin.reflect.KType import org.bson.BsonDocument as OfficialBsonDocument @@ -31,6 +41,7 @@ import org.bson.BsonDocument as OfficialBsonDocument * * To create an instance of this class, see [BsonFactory.readDocument]. */ +@Serializable(with = BsonDocument.Serializer::class) actual class BsonDocument internal constructor( val raw: OfficialBsonDocument, actual override val factory: BsonFactory, @@ -45,10 +56,12 @@ actual class BsonDocument internal constructor( val codec = factory.findCodecForType(type) return try { - codec.decode( - raw.asBsonReader(), - DecoderContext.builder().build(), - ) + BsonFactory.setCurrent(factory) { + codec.decode( + raw.asBsonReader(), + DecoderContext.builder().build(), + ) + } } catch (e: Exception) { throw BsonDecodingException("Could not decode $type\n\tfrom value $this\n\tusing $codec", e) } @@ -244,6 +257,28 @@ actual class BsonDocument internal constructor( override fun toString(): String = "($name, $value)" } + + @OptIn(ExperimentalSerializationApi::class) + actual object Serializer : KSerializer { + private const val NAME = "opensavvy.ktmongo.bson.official.BsonDocument" + + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor(NAME, PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: BsonDocument) { + require(encoder is BsonEncoder) { "${this::class} only supports org.bson:bson-kotlinx. See its documentation for details. Found encoder: $encoder" } + + encoder.encodeBsonValue(value.raw) + } + + @OptIn(LowLevelApi::class) + override fun deserialize(decoder: Decoder): BsonDocument { + require(decoder is BsonDecoder) { "${this::class} only supports org.bson:bson-kotlinx. See its documentation for details. Found decoder: $decoder" } + + val decoded = decoder.decodeBsonValue() + return BsonDocument(decoded as OfficialBsonDocument, BsonFactory.current()) + } + } } // Inspired by https://gist.github.com/Koboo/ebd7c6802101e1a941ef31baca04113d diff --git a/bson-official/src/jvmMain/kotlin/BsonFactory.jvm.kt b/bson-official/src/jvmMain/kotlin/BsonFactory.jvm.kt index 59867d9d..2aa4c5c7 100644 --- a/bson-official/src/jvmMain/kotlin/BsonFactory.jvm.kt +++ b/bson-official/src/jvmMain/kotlin/BsonFactory.jvm.kt @@ -18,6 +18,8 @@ package opensavvy.ktmongo.bson.official import opensavvy.ktmongo.bson.* import opensavvy.ktmongo.bson.BsonFactory +import opensavvy.ktmongo.bson.official.BsonFactory.Companion.current +import opensavvy.ktmongo.bson.official.BsonFactory.Companion.setCurrent import opensavvy.ktmongo.bson.official.types.* import opensavvy.ktmongo.dsl.LowLevelApi import org.bson.BsonBinaryReader @@ -68,6 +70,7 @@ actual class BsonFactory( CodecRegistries.fromCodecs( KotlinBsonDocumentCodec(this), KotlinCommonBsonDocumentCodec(this), + KotlinBsonValueCodec(this), KotlinBsonArrayCodec(this), KotlinObjectIdCodec(), KotlinTimestampCodec(), @@ -103,7 +106,7 @@ actual class BsonFactory( } @LowLevelApi - actual override fun buildDocument(instance: BsonFieldWriteable): BsonDocument = + actual override fun buildDocument(instance: BsonFieldWriteable): BsonDocument = buildDocument { instance.writeTo(this) } @LowLevelApi @@ -234,4 +237,42 @@ actual class BsonFactory( @LowLevelApi inline fun findCodecForType(): Codec = findCodecForType(typeOf()) + + companion object { + private val currentFactory = ThreadLocal() + + /** + * Accesses the currently-set [opensavvy.ktmongo.bson.official.BsonFactory]. + * + * In most situations, no current factory is set, and this method throws [IllegalStateException]. + * + * This method is part of a work-around to deserialize [BsonDocument] & co. + * These types are themselves deserializable, so they need to know which factory they are created with. + * However, KotlinX.Serialization does not pass that information to the deserializer. + * + * Instead, we call [setCurrent] when deserializing, so the deserializer has access to the factory + * at the top of the stacktrace. + */ + @LowLevelApi + internal fun current(): opensavvy.ktmongo.bson.official.BsonFactory = + currentFactory.get() + ?: error("The current BSON factory is not set. This usually means you are deserializing a special type like BsonDocument from a function that is not KtMongo aware. If that's your situation, try deserializing with one of the decode() methods provided by the KtMongo library. If you cannot use these methods, please report your use-case.") + + /** + * Sets [current] to [factory] during the lifetime of [block]. + */ + @LowLevelApi + internal fun setCurrent( + factory: opensavvy.ktmongo.bson.official.BsonFactory, + block: () -> T, + ): T { + currentFactory.set(factory) + try { + return block() + } finally { + currentFactory.remove() + // We don't reset to the value from before the call, because it was 'null' anyway + } + } + } } diff --git a/bson-official/src/jvmMain/kotlin/BsonValue.jvm.kt b/bson-official/src/jvmMain/kotlin/BsonValue.jvm.kt index 35607cb6..b8330e01 100644 --- a/bson-official/src/jvmMain/kotlin/BsonValue.jvm.kt +++ b/bson-official/src/jvmMain/kotlin/BsonValue.jvm.kt @@ -16,9 +16,18 @@ package opensavvy.ktmongo.bson.official +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +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 opensavvy.ktmongo.bson.* import opensavvy.ktmongo.bson.BsonDocument import opensavvy.ktmongo.bson.BsonValue +import opensavvy.ktmongo.bson.official.BsonValue.Serializer import opensavvy.ktmongo.bson.official.types.toKtMongo import opensavvy.ktmongo.bson.types.ObjectId import opensavvy.ktmongo.bson.types.Timestamp @@ -28,6 +37,8 @@ import org.bson.BsonWriter import org.bson.codecs.Codec import org.bson.codecs.DecoderContext import org.bson.codecs.EncoderContext +import org.bson.codecs.kotlinx.BsonDecoder +import org.bson.codecs.kotlinx.BsonEncoder import java.nio.ByteBuffer import kotlin.reflect.KClass import kotlin.reflect.KType @@ -37,6 +48,7 @@ import kotlin.reflect.KType * * To create an instance of this class, see [BsonFactory.readValue]. */ +@Serializable(with = Serializer::class) actual class BsonValue internal constructor( val raw: org.bson.BsonValue, actual override val factory: BsonFactory, @@ -226,8 +238,31 @@ actual class BsonValue internal constructor( override fun hashCode(): Int { return BsonValue.hashCode(this) } + + @OptIn(ExperimentalSerializationApi::class) + actual object Serializer : KSerializer { + private const val NAME = "opensavvy.ktmongo.bson.official.BsonValue" + + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor(NAME, PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: opensavvy.ktmongo.bson.official.BsonValue) { + require(encoder is BsonEncoder) { "${this::class} only supports org.bson:bson-kotlinx. See its documentation for details. Found encoder: $encoder" } + + encoder.encodeBsonValue(value.raw) + } + + @OptIn(LowLevelApi::class) + override fun deserialize(decoder: Decoder): opensavvy.ktmongo.bson.official.BsonValue { + require(decoder is BsonDecoder) { "${this::class} only supports org.bson:bson-kotlinx. See its documentation for details. Found decoder: $decoder" } + + val decoded = decoder.decodeBsonValue() + return BsonValue(decoded, BsonFactory.current()) + } + } } +@OptIn(LowLevelApi::class) internal fun decodeValue( value: org.bson.BsonValue, kClass: KClass, @@ -246,7 +281,9 @@ internal fun decodeValue( try { // Decode the fake document and extract its only field using a delegating codec. val docCodec = FakeDocumentCodec(valueCodec) - val decoded = docCodec.decode(documentReader, DecoderContext.builder().build()) + val decoded = BsonFactory.setCurrent(factory) { + docCodec.decode(documentReader, DecoderContext.builder().build()) + } return decoded.a } catch (e: Exception) { throw BsonDecodingException("Could not decode $kClass\n\tfrom value ${factory.readValue(value)}\n\tusing $valueCodec", e) diff --git a/bson-official/src/jvmMain/kotlin/types/BsonValue.jvm.kt b/bson-official/src/jvmMain/kotlin/types/BsonValue.jvm.kt new file mode 100644 index 00000000..f29eee74 --- /dev/null +++ b/bson-official/src/jvmMain/kotlin/types/BsonValue.jvm.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2026, OpenSavvy and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package opensavvy.ktmongo.bson.official.types + +import opensavvy.ktmongo.bson.official.BsonFactory +import opensavvy.ktmongo.bson.official.BsonValue +import org.bson.BsonReader +import org.bson.BsonWriter +import org.bson.codecs.BsonValueCodec +import org.bson.codecs.Codec +import org.bson.codecs.DecoderContext +import org.bson.codecs.EncoderContext + +internal class KotlinBsonValueCodec( + private val factory: BsonFactory, +) : Codec { + private val valueCodec = BsonValueCodec() + + override fun encode(writer: BsonWriter, value: BsonValue, encoderContext: EncoderContext) { + valueCodec.encode(writer, value.raw, encoderContext) + } + + override fun getEncoderClass(): Class = + BsonValue::class.java + + override fun decode(reader: BsonReader, decoderContext: DecoderContext): BsonValue? = + valueCodec.decode(reader, decoderContext)?.let { BsonValue(it, factory) } + +} diff --git a/bson-official/src/jvmTest/kotlin/OfficialBsonFactory.jvm.kt b/bson-official/src/jvmTest/kotlin/OfficialBsonFactory.jvm.kt index 06b98a5b..f407d625 100644 --- a/bson-official/src/jvmTest/kotlin/OfficialBsonFactory.jvm.kt +++ b/bson-official/src/jvmTest/kotlin/OfficialBsonFactory.jvm.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, OpenSavvy and contributors. + * Copyright (c) 2025-2026, OpenSavvy and contributors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -150,6 +150,7 @@ val OfficialJvmBsonFactory by preparedSuite { check(testDocument().decode().name == "Bob") } + serializeIntoDocumentTypes(reflectionFactory) verifyBsonFactory(reflectionFactory) } @@ -174,6 +175,7 @@ val OfficialJvmBsonFactory by preparedSuite { check(testDocument().decode().name == "Bob") } + serializeIntoDocumentTypes(serializationFactory) verifyBsonFactory(serializationFactory) } } diff --git a/bson-official/src/jvmTest/kotlin/SerializeIntoDocumentTypes.kt b/bson-official/src/jvmTest/kotlin/SerializeIntoDocumentTypes.kt new file mode 100644 index 00000000..4b1fa15a --- /dev/null +++ b/bson-official/src/jvmTest/kotlin/SerializeIntoDocumentTypes.kt @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2026, OpenSavvy and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(LowLevelApi::class, ExperimentalBsonDiffApi::class) + +package opensavvy.ktmongo.bson.official + +import kotlinx.serialization.Serializable +import opensavvy.ktmongo.bson.ExperimentalBsonDiffApi +import opensavvy.ktmongo.bson.decode +import opensavvy.ktmongo.bson.diff +import opensavvy.ktmongo.bson.encode +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.prepared.suite.Prepared +import opensavvy.prepared.suite.SuiteDsl + +@Serializable +data class NestedDocument( + val data: BsonDocument, +) + +@Serializable +data class NestedArray( + val data: BsonArray, +) + +@Serializable +data class NestedValues( + val values: List, +) + +fun SuiteDsl.serializeIntoDocumentTypes(factory: Prepared) = suite("Nested BsonDocument & co") { + + test("BsonDocument") { + val document = factory().buildDocument { + writeDocument("data") { + writeString("a", "1") + writeInt32("b", 2) + } + } + + val decoded = document.decode() + + check(decoded.data["a"]?.decodeString() == "1") + check(decoded.data["b"]?.decodeInt32() == 2) + + val encoded = factory().encode(decoded) + + check(document == encoded) { "Diff: ${document diff encoded}" } + } + + test("BsonArray") { + val document = factory().buildDocument { + writeArray("data") { + writeString("1") + writeInt32(2) + } + } + + val decoded = document.decode() + + check(decoded.data[0]?.decodeString() == "1") + check(decoded.data[1]?.decodeInt32() == 2) + + val encoded = factory().encode(decoded) + + check(document == encoded) { "Diff: ${document diff encoded}" } + } + + test("BsonValue") { + val document = factory().buildDocument { + writeArray("values") { + writeString("1") + writeInt32(2) + writeDocument { + writeString("a", "1") + writeInt32("b", 2) + } + writeArray { + writeBoolean(true) + writeBoolean(false) + } + } + } + + val decoded = document.decode() + + check(decoded.values[0].decodeString() == "1") + check(decoded.values[1].decodeInt32() == 2) + check(decoded.values[2].decodeDocument()["a"]?.decodeString() == "1") + check(decoded.values[2].decodeDocument()["b"]?.decodeInt32() == 2) + check(decoded.values[3].decodeArray()[0]?.decodeBoolean() == true) + check(decoded.values[3].decodeArray()[1]?.decodeBoolean() == false) + + val encoded = factory().encode(decoded) + + check(document == encoded) { "Diff: ${document diff encoded}" } + } +} -- 2.51.2 From 617773316803eed49bdca73af81edc9c78a1bed9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Tue, 5 May 2026 19:15:23 +0200 Subject: [PATCH 03/10] test(bson): Implementations should be able to serialize/deserialize the BsonDocument & co interfaces --- .../src/commonMain/kotlin/BsonArrayTests.kt | 33 +++++++++++- .../commonMain/kotlin/BsonDocumentTests.kt | 47 +++++++++++++++++ .../src/commonMain/kotlin/BsonValueTests.kt | 51 +++++++++++++++++++ .../src/commonMain/kotlin/BsonWriterTest.kt | 1 + 4 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 bson-tests/src/commonMain/kotlin/BsonValueTests.kt diff --git a/bson-tests/src/commonMain/kotlin/BsonArrayTests.kt b/bson-tests/src/commonMain/kotlin/BsonArrayTests.kt index 14577685..b278706f 100644 --- a/bson-tests/src/commonMain/kotlin/BsonArrayTests.kt +++ b/bson-tests/src/commonMain/kotlin/BsonArrayTests.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -@file:OptIn(LowLevelApi::class) +@file:OptIn(LowLevelApi::class, ExperimentalBsonPathApi::class) @file:Suppress("SENSELESS_COMPARISON") package opensavvy.ktmongo.bson @@ -40,6 +40,12 @@ data class BsonArrayUser( val friends: List, ) +@Serializable +data class BsonArrayNested( + val id: String, + val nested: BsonArray, +) + fun SuiteDsl.verifyBsonArrays(factory: Prepared) = suite("BSON arrays") { test("Decode a simple array") { @@ -186,6 +192,31 @@ fun SuiteDsl.verifyBsonArrays(factory: Prepared) = suite("BSON arra check(user.decode() == BsonArrayUser("123456", BsonArrayProfile("Bob", 12.0), listOf(BsonArrayProfile("Alice", 13.7), BsonArrayProfile("Charlie", 14.5)))) } + test("Encode and decode a nested BsonArray") { + val nested = factory().buildArray { + writeDocument { + writeString("name", "Bob") + writeDouble("score", 12.0) + } + + writeDocument { + writeString("name", "Alice") + writeDouble("score", 13.7) + } + } + + val wrapped = BsonArrayNested("ao", nested) + + check(wrapped.nested.decodeElements() == listOf(BsonArrayProfile("Bob", 12.0), BsonArrayProfile("Alice", 13.7))) + + val encoded = factory().buildDocument { + writeSafe("d", wrapped) + } + + check(encoded.selectFirst("$.d.nested[0].name") == "Bob") + check(encoded["d"]?.decode()?.nested == nested) + } + suite("Iteration") { test("Iterable") { val array = factory().buildArray { diff --git a/bson-tests/src/commonMain/kotlin/BsonDocumentTests.kt b/bson-tests/src/commonMain/kotlin/BsonDocumentTests.kt index 4f18b61d..d7eefe43 100644 --- a/bson-tests/src/commonMain/kotlin/BsonDocumentTests.kt +++ b/bson-tests/src/commonMain/kotlin/BsonDocumentTests.kt @@ -32,6 +32,12 @@ data class BsonDocumentUser( val b: Int, ) +@Serializable +data class BsonDocumentNested( + val name: String, + val nested: BsonDocument, +) + fun SuiteDsl.verifyBsonDocuments(factory: Prepared) = suite("BSON documents") { test("Decode a simple document") { @@ -43,6 +49,47 @@ fun SuiteDsl.verifyBsonDocuments(factory: Prepared) = suite("BSON d check(document.decode() == BsonDocumentUser("Bob", 45)) } + test("Decode a simple document as a BsonDocument instance") { + val document = factory().buildDocument { + writeString("a", "Bob") + writeInt32("b", 45) + } + + check(document.decode() == document) + } + + test("Encode a simple document as a BsonDocument instance") { + val document = factory().buildDocument { + writeString("a", "Bob") + writeInt32("b", 45) + } + + val wrapped = factory().buildDocument { + writeSafe("d", document) + } + + check(wrapped.selectFirst("$.d.a") == "Bob") + check(wrapped["d"]?.decode() == document) + } + + test("Encode and decode a nested BsonDocument") { + val nested = factory().buildDocument { + writeString("a", "Bob") + writeInt32("b", 45) + } + + val wrapped = BsonDocumentNested( + name = "Test", + nested = nested, + ) + + val encoded = factory().buildDocument { + writeSafe("d", wrapped) + } + + check(encoded["d"]?.decode()?.nested == nested) + } + // TODO in https://gitlab.com/opensavvy/ktmongo/-/work_items/119 // Bugged in org.bson:bson-kotlin // test("Decode a single document with an optional field") { diff --git a/bson-tests/src/commonMain/kotlin/BsonValueTests.kt b/bson-tests/src/commonMain/kotlin/BsonValueTests.kt new file mode 100644 index 00000000..5fbb23e3 --- /dev/null +++ b/bson-tests/src/commonMain/kotlin/BsonValueTests.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2026, OpenSavvy and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(LowLevelApi::class, ExperimentalBsonPathApi::class) + +package opensavvy.ktmongo.bson + +import kotlinx.serialization.Serializable +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.prepared.suite.Prepared +import opensavvy.prepared.suite.SuiteDsl + +@Serializable +data class BsonValueNested( + val id: String, + val nested: BsonValue, +) + +fun SuiteDsl.verifyBsonValues(factory: Prepared) = suite("BSON values") { + + test("Encode and decode a nested BsonValue") { + val nested = factory().buildDocument { + writeString("v", "foo") + }["v"]!! + + val wrapped = BsonValueNested("e", nested) + + check(wrapped.nested.decodeString() == "foo") + + val encoded = factory().buildDocument { + writeSafe("d", wrapped) + } + + check(encoded.selectFirst("$.d.nested") == "foo") + check(encoded["d"]?.decode()?.nested == nested) + } + +} diff --git a/bson-tests/src/commonMain/kotlin/BsonWriterTest.kt b/bson-tests/src/commonMain/kotlin/BsonWriterTest.kt index f42fc998..64264889 100644 --- a/bson-tests/src/commonMain/kotlin/BsonWriterTest.kt +++ b/bson-tests/src/commonMain/kotlin/BsonWriterTest.kt @@ -32,6 +32,7 @@ fun SuiteDsl.verifyBsonFactory( ) { verifyBsonDocuments(prepareFactory) verifyBsonArrays(prepareFactory) + verifyBsonValues(prepareFactory) test("An Int in a root document") { val result = prepareFactory().buildDocument { -- 2.51.2 From 33f44a77d64f6fbf2103e3bed1d0f738c93902cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Mon, 11 May 2026 19:56:35 +0200 Subject: [PATCH 04/10] feat(bson-official): Serialize the common BsonDocument, BsonArray and BsonValue (reflection-based) --- .../src/jvmMain/kotlin/BsonFactory.jvm.kt | 2 ++ .../src/jvmMain/kotlin/types/BsonArray.jvm.kt | 25 ++++++++++++++++--- .../src/jvmMain/kotlin/types/BsonValue.jvm.kt | 17 +++++++++++++ 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/bson-official/src/jvmMain/kotlin/BsonFactory.jvm.kt b/bson-official/src/jvmMain/kotlin/BsonFactory.jvm.kt index 2aa4c5c7..a468bb03 100644 --- a/bson-official/src/jvmMain/kotlin/BsonFactory.jvm.kt +++ b/bson-official/src/jvmMain/kotlin/BsonFactory.jvm.kt @@ -71,7 +71,9 @@ actual class BsonFactory( KotlinBsonDocumentCodec(this), KotlinCommonBsonDocumentCodec(this), KotlinBsonValueCodec(this), + KotlinCommonBsonValueCodec(this), KotlinBsonArrayCodec(this), + KotlinCommonBsonArrayCodec(this), KotlinObjectIdCodec(), KotlinTimestampCodec(), KotlinUuidCodec(), diff --git a/bson-official/src/jvmMain/kotlin/types/BsonArray.jvm.kt b/bson-official/src/jvmMain/kotlin/types/BsonArray.jvm.kt index b4328526..08815693 100644 --- a/bson-official/src/jvmMain/kotlin/types/BsonArray.jvm.kt +++ b/bson-official/src/jvmMain/kotlin/types/BsonArray.jvm.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, OpenSavvy and contributors. + * Copyright (c) 2024-2026, OpenSavvy and contributors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,16 +28,33 @@ import org.bson.codecs.EncoderContext internal class KotlinBsonArrayCodec( private val factory: BsonFactory, ) : Codec { - private val documentCodec = BsonArrayCodec() + private val arrayCodec = BsonArrayCodec() override fun encode(writer: BsonWriter, value: BsonArray, encoderContext: EncoderContext) { - documentCodec.encode(writer, value.raw, encoderContext) + arrayCodec.encode(writer, value.raw, encoderContext) } override fun getEncoderClass(): Class = BsonArray::class.java override fun decode(reader: BsonReader, decoderContext: DecoderContext): BsonArray? = - documentCodec.decode(reader, decoderContext)?.let { BsonArray(it, factory) } + arrayCodec.decode(reader, decoderContext)?.let { BsonArray(it, factory) } + +} + +internal class KotlinCommonBsonArrayCodec( + private val factory: BsonFactory, +) : Codec { + private val arrayCodec = BsonArrayCodec() + + override fun encode(writer: BsonWriter, value: opensavvy.ktmongo.bson.BsonArray, encoderContext: EncoderContext) { + arrayCodec.encode(writer, (value as BsonArray).raw, encoderContext) + } + + override fun getEncoderClass(): Class = + opensavvy.ktmongo.bson.BsonArray::class.java + + override fun decode(reader: BsonReader, decoderContext: DecoderContext): opensavvy.ktmongo.bson.BsonArray? = + arrayCodec.decode(reader, decoderContext)?.let { BsonArray(it, factory) } } diff --git a/bson-official/src/jvmMain/kotlin/types/BsonValue.jvm.kt b/bson-official/src/jvmMain/kotlin/types/BsonValue.jvm.kt index f29eee74..7b83c5ba 100644 --- a/bson-official/src/jvmMain/kotlin/types/BsonValue.jvm.kt +++ b/bson-official/src/jvmMain/kotlin/types/BsonValue.jvm.kt @@ -41,3 +41,20 @@ internal class KotlinBsonValueCodec( valueCodec.decode(reader, decoderContext)?.let { BsonValue(it, factory) } } + +internal class KotlinCommonBsonValueCodec( + private val factory: BsonFactory, +) : Codec { + private val valueCodec = BsonValueCodec() + + override fun encode(writer: BsonWriter, value: opensavvy.ktmongo.bson.BsonValue, encoderContext: EncoderContext) { + valueCodec.encode(writer, (value as BsonValue).raw, encoderContext) + } + + override fun getEncoderClass(): Class = + opensavvy.ktmongo.bson.BsonValue::class.java + + override fun decode(reader: BsonReader, decoderContext: DecoderContext): opensavvy.ktmongo.bson.BsonValue? = + valueCodec.decode(reader, decoderContext)?.let { BsonValue(it, factory) } + +} -- 2.51.2 From b81e8dd488655267e7f43a44b8a699c4986b27b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Thu, 14 May 2026 12:22:18 +0200 Subject: [PATCH 05/10] feat(bson): Mark BsonDocument, BsonArray and BsonValue for contextual serialization --- bson/src/commonMain/kotlin/BsonArray.kt | 6 ++++++ bson/src/commonMain/kotlin/BsonDocument.kt | 6 ++++++ bson/src/commonMain/kotlin/BsonValue.kt | 6 ++++++ 3 files changed, 18 insertions(+) diff --git a/bson/src/commonMain/kotlin/BsonArray.kt b/bson/src/commonMain/kotlin/BsonArray.kt index cd7ec1e2..2b93f1bb 100644 --- a/bson/src/commonMain/kotlin/BsonArray.kt +++ b/bson/src/commonMain/kotlin/BsonArray.kt @@ -16,6 +16,9 @@ package opensavvy.ktmongo.bson +import kotlinx.serialization.ContextualSerializer +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable import opensavvy.ktmongo.dsl.LowLevelApi import kotlin.reflect.KType import kotlin.reflect.typeOf @@ -51,6 +54,9 @@ import kotlin.reflect.typeOf * The methods [BsonArray.Companion.equals] and [BsonArray.Companion.hashCode] are provided * as default implementations. */ +@OptIn(ExperimentalSerializationApi::class) +@Suppress("SERIALIZER_TYPE_INCOMPATIBLE") +@Serializable(with = ContextualSerializer::class) interface BsonArray { /** diff --git a/bson/src/commonMain/kotlin/BsonDocument.kt b/bson/src/commonMain/kotlin/BsonDocument.kt index 3b7782ef..787e376c 100644 --- a/bson/src/commonMain/kotlin/BsonDocument.kt +++ b/bson/src/commonMain/kotlin/BsonDocument.kt @@ -16,6 +16,9 @@ package opensavvy.ktmongo.bson +import kotlinx.serialization.ContextualSerializer +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable import opensavvy.ktmongo.dsl.LowLevelApi import kotlin.reflect.KType import kotlin.reflect.typeOf @@ -54,6 +57,9 @@ import kotlin.reflect.typeOf * The methods [BsonDocument.Companion.equals] and [BsonDocument.Companion.hashCode] are provided * as default implementations. */ +@OptIn(ExperimentalSerializationApi::class) +@Suppress("SERIALIZER_TYPE_INCOMPATIBLE") +@Serializable(with = ContextualSerializer::class) interface BsonDocument { /** diff --git a/bson/src/commonMain/kotlin/BsonValue.kt b/bson/src/commonMain/kotlin/BsonValue.kt index 4943fd8f..4f530572 100644 --- a/bson/src/commonMain/kotlin/BsonValue.kt +++ b/bson/src/commonMain/kotlin/BsonValue.kt @@ -16,6 +16,9 @@ package opensavvy.ktmongo.bson +import kotlinx.serialization.ContextualSerializer +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable import opensavvy.ktmongo.bson.types.ObjectId import opensavvy.ktmongo.bson.types.Timestamp import opensavvy.ktmongo.bson.types.Vector @@ -60,6 +63,9 @@ import kotlin.time.Instant * The methods [BsonValue.Companion.equals] and [BsonValue.Companion.hashCode] are provided * as default implementations. */ +@OptIn(ExperimentalSerializationApi::class) +@Suppress("SERIALIZER_TYPE_INCOMPATIBLE") +@Serializable(with = ContextualSerializer::class) interface BsonValue { /** -- 2.51.2 From 280cab5d03fbce6d34bc10da5dfaf0661d1232b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Mon, 11 May 2026 20:33:38 +0200 Subject: [PATCH 06/10] refactor(bson-official): Instantiate the codecs as a list --- .../src/jvmMain/kotlin/BsonFactory.jvm.kt | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/bson-official/src/jvmMain/kotlin/BsonFactory.jvm.kt b/bson-official/src/jvmMain/kotlin/BsonFactory.jvm.kt index a468bb03..6feedb99 100644 --- a/bson-official/src/jvmMain/kotlin/BsonFactory.jvm.kt +++ b/bson-official/src/jvmMain/kotlin/BsonFactory.jvm.kt @@ -66,8 +66,8 @@ actual class BsonFactory( * * If you only use the KtMongo DSL to write queries, you do not need this configuration. */ - val codecRegistry: CodecRegistry = CodecRegistries.fromRegistries( - CodecRegistries.fromCodecs( + val codecRegistry: CodecRegistry = run { + val kotlinCodecs = arrayListOf( KotlinBsonDocumentCodec(this), KotlinCommonBsonDocumentCodec(this), KotlinBsonValueCodec(this), @@ -90,9 +90,13 @@ actual class BsonFactory( KotlinPrimitiveDoubleCodec(), KotlinPrimitiveBooleanCodec(), KotlinPrimitiveCharCodec(), - ), - codecRegistry, - ) + ) + + CodecRegistries.fromRegistries( + CodecRegistries.fromCodecs(kotlinCodecs), + codecRegistry, + ) + } @LowLevelApi actual override fun buildDocument(block: BsonFieldWriter.() -> Unit): BsonDocument { -- 2.51.2 From f4aeb350cca3088015fbb3fe4c2a74e8d8656e86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Thu, 14 May 2026 13:42:48 +0200 Subject: [PATCH 07/10] feat(bson-official): Serialize the common BsonDocument, BsonArray and BsonValue (serialization-based) --- .../src/jvmMain/kotlin/BsonFactory.jvm.kt | 3 +- .../KotlinSerializerCodecProviderInjector.kt | 72 ++++++++++++++ .../KotlinXSerializersModule.jvm.kt | 97 +++++++++++++++++++ 3 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 bson-official/src/jvmMain/kotlin/serialization/KotlinSerializerCodecProviderInjector.kt create mode 100644 bson-official/src/jvmMain/kotlin/serialization/KotlinXSerializersModule.jvm.kt diff --git a/bson-official/src/jvmMain/kotlin/BsonFactory.jvm.kt b/bson-official/src/jvmMain/kotlin/BsonFactory.jvm.kt index 6feedb99..7c7e5bb0 100644 --- a/bson-official/src/jvmMain/kotlin/BsonFactory.jvm.kt +++ b/bson-official/src/jvmMain/kotlin/BsonFactory.jvm.kt @@ -20,6 +20,7 @@ import opensavvy.ktmongo.bson.* import opensavvy.ktmongo.bson.BsonFactory import opensavvy.ktmongo.bson.official.BsonFactory.Companion.current import opensavvy.ktmongo.bson.official.BsonFactory.Companion.setCurrent +import opensavvy.ktmongo.bson.official.serialization.KotlinSerializerCodecProviderInjector import opensavvy.ktmongo.bson.official.types.* import opensavvy.ktmongo.dsl.LowLevelApi import org.bson.BsonBinaryReader @@ -94,7 +95,7 @@ actual class BsonFactory( CodecRegistries.fromRegistries( CodecRegistries.fromCodecs(kotlinCodecs), - codecRegistry, + KotlinSerializerCodecProviderInjector.injectIfPresent(codecRegistry), ) } diff --git a/bson-official/src/jvmMain/kotlin/serialization/KotlinSerializerCodecProviderInjector.kt b/bson-official/src/jvmMain/kotlin/serialization/KotlinSerializerCodecProviderInjector.kt new file mode 100644 index 00000000..88f51096 --- /dev/null +++ b/bson-official/src/jvmMain/kotlin/serialization/KotlinSerializerCodecProviderInjector.kt @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2026, OpenSavvy and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package opensavvy.ktmongo.bson.official.serialization + +import kotlinx.serialization.Serializable +import opensavvy.ktmongo.bson.official.BsonArray +import opensavvy.ktmongo.bson.official.BsonDocument +import opensavvy.ktmongo.bson.official.BsonFactory +import opensavvy.ktmongo.bson.official.BsonValue +import opensavvy.ktmongo.bson.official.serialization.KotlinSerializerCodecProviderInjector.isOfficialKotlinSerializationInClasspath +import org.bson.codecs.configuration.CodecConfigurationException +import org.bson.codecs.configuration.CodecRegistries +import org.bson.codecs.configuration.CodecRegistry +import org.bson.codecs.kotlinx.KotlinSerializerCodecProvider + +// A @Serializable class that no standard MongoDB codec provider handles except KotlinSerializerCodecProvider. +// Used as a probe to detect whether the user's registry contains one. +@Serializable +private class KtMongoSerializationProbe + +/** + * Detects whether a [CodecRegistry] contains a [KotlinSerializerCodecProvider], and if so, prepends a new + * [KotlinSerializerCodecProvider] configured with [ktMongoSerializersModule] so that KtMongo BSON types + * ([BsonDocument], [BsonArray], [BsonValue]) are handled correctly when used as polymorphic fields. + * + * The user's original provider is not removed; it remains in the chain and handles types that are not in + * [ktMongoSerializersModule]. If the user configured a custom [org.bson.codecs.kotlinx.BsonConfiguration], + * it will not be inherited by the injected provider. Since the provider only handles + * [BsonDocument], [BsonArray], [BsonValue], it would not configure anything meaningful anyway. + * + * This object is intentionally isolated from [BsonFactory] so that the JVM only loads + * [KotlinSerializerCodecProvider] (a `compileOnly` dependency) when this object is first accessed — + * which only happens when [isOfficialKotlinSerializationInClasspath] is true. + */ +internal object KotlinSerializerCodecProviderInjector { + + fun injectIfPresent(registry: CodecRegistry): CodecRegistry = + if (!isOfficialKotlinSerializationInClasspath || !hasKotlinSerializerCodecProvider(registry)) + registry + else CodecRegistries.fromRegistries( + CodecRegistries.fromProviders(KotlinSerializerCodecProvider(ktMongoSerializersModule)), + registry, + ) + + private val isOfficialKotlinSerializationInClasspath: Boolean = try { + Class.forName("org.bson.codecs.kotlinx.BsonEncoder") + true + } catch (_: ClassNotFoundException) { + false + } + + private fun hasKotlinSerializerCodecProvider(registry: CodecRegistry): Boolean = try { + registry.get(KtMongoSerializationProbe::class.java) + true + } catch (_: CodecConfigurationException) { + false + } +} diff --git a/bson-official/src/jvmMain/kotlin/serialization/KotlinXSerializersModule.jvm.kt b/bson-official/src/jvmMain/kotlin/serialization/KotlinXSerializersModule.jvm.kt new file mode 100644 index 00000000..871cd749 --- /dev/null +++ b/bson-official/src/jvmMain/kotlin/serialization/KotlinXSerializersModule.jvm.kt @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2026, OpenSavvy and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package opensavvy.ktmongo.bson.official.serialization + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.modules.contextual +import kotlinx.serialization.modules.plus +import opensavvy.ktmongo.bson.official.BsonArray +import opensavvy.ktmongo.bson.official.BsonDocument +import opensavvy.ktmongo.bson.official.BsonFactory +import opensavvy.ktmongo.bson.official.BsonValue +import org.bson.codecs.kotlinx.defaultSerializersModule as bsonKotlinxDefaultSerializersModule + +// Contextual serializers for the common interfaces, delegating to the concrete JVM serializers. +// The common interfaces are annotated with @Serializable(with = ContextualSerializer::class), so the +// kotlinx.serialization plugin looks these up at runtime from this module rather than generating +// polymorphic (discriminator-wrapped) code. + +private object CommonBsonDocumentSerializer : KSerializer { + override val descriptor: SerialDescriptor = BsonDocument.Serializer.descriptor + + override fun serialize(encoder: Encoder, value: opensavvy.ktmongo.bson.BsonDocument) = + BsonDocument.Serializer.serialize(encoder, value as BsonDocument) + + override fun deserialize(decoder: Decoder): opensavvy.ktmongo.bson.BsonDocument = + BsonDocument.Serializer.deserialize(decoder) +} + +private object CommonBsonArraySerializer : KSerializer { + override val descriptor: SerialDescriptor = BsonArray.Serializer.descriptor + + override fun serialize(encoder: Encoder, value: opensavvy.ktmongo.bson.BsonArray) = + BsonArray.Serializer.serialize(encoder, value as BsonArray) + + override fun deserialize(decoder: Decoder): opensavvy.ktmongo.bson.BsonArray = + BsonArray.Serializer.deserialize(decoder) +} + +private object CommonBsonValueSerializer : KSerializer { + override val descriptor: SerialDescriptor = BsonValue.Serializer.descriptor + + override fun serialize(encoder: Encoder, value: opensavvy.ktmongo.bson.BsonValue) = + BsonValue.Serializer.serialize(encoder, value as BsonValue) + + override fun deserialize(decoder: Decoder): opensavvy.ktmongo.bson.BsonValue = + BsonValue.Serializer.deserialize(decoder) +} + +/** + * A [SerializersModule] that registers KtMongo BSON types for KotlinX.Serialization. + * + * Use this when building a [BsonFactory] with [org.bson.codecs.kotlinx.KotlinSerializerCodecProvider] + * so that fields typed as [opensavvy.ktmongo.bson.BsonDocument], + * [opensavvy.ktmongo.bson.BsonArray], or [opensavvy.ktmongo.bson.BsonValue] are serialized correctly: + * + * ```kotlin + * val factory = BsonFactory(CodecRegistries.fromProviders( + * KotlinSerializerCodecProvider(ktMongoSerializersModule), + * ... + * )) + * ``` + * + * Fields typed as the common interfaces are serialized as-is (no type discriminator): + * + * ```kotlin + * @Serializable + * data class MyDoc( + * val nested: BsonDocument, + * val array: BsonArray, + * val value: BsonValue, + * ) + * ``` + */ +@OptIn(kotlinx.serialization.ExperimentalSerializationApi::class) +internal val ktMongoSerializersModule: SerializersModule = bsonKotlinxDefaultSerializersModule + SerializersModule { + contextual(CommonBsonDocumentSerializer) + contextual(CommonBsonArraySerializer) + contextual(CommonBsonValueSerializer) +} -- 2.51.2 From da6449a52f5da6652c151e520664765ae1301dde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Thu, 14 May 2026 14:00:51 +0200 Subject: [PATCH 08/10] feat(bson-multiplatform): Allow configuring the SerializersModule when creating a BsonFactory --- .../src/commonMain/kotlin/BsonArray.kt | 3 +- .../src/commonMain/kotlin/BsonDocument.kt | 3 +- .../src/commonMain/kotlin/BsonFactory.kt | 32 ++++++++-- .../src/commonMain/kotlin/BsonValue.kt | 3 +- .../write/MultiplatformDocumentFieldWriter.kt | 11 ++-- .../serialization/MultiplatformDecoder.kt | 45 ++++++++------ .../serialization/MultiplatformEncoder.kt | 62 +++++++++++++------ 7 files changed, 105 insertions(+), 54 deletions(-) diff --git a/bson-multiplatform/src/commonMain/kotlin/BsonArray.kt b/bson-multiplatform/src/commonMain/kotlin/BsonArray.kt index 69235520..848788b8 100644 --- a/bson-multiplatform/src/commonMain/kotlin/BsonArray.kt +++ b/bson-multiplatform/src/commonMain/kotlin/BsonArray.kt @@ -24,7 +24,6 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder -import kotlinx.serialization.modules.EmptySerializersModule import kotlinx.serialization.serializer import opensavvy.ktmongo.bson.BsonArray import opensavvy.ktmongo.bson.BsonDecodingException @@ -80,7 +79,7 @@ class BsonArray internal constructor( @ExperimentalSerializationApi @LowLevelApi override fun decode(type: KType): T { - val decoder = BsonDecoderTopLevel(EmptySerializersModule(), factory, bytesWithHeader) + val decoder = BsonDecoderTopLevel(factory, bytesWithHeader) @Suppress("UNCHECKED_CAST") val serializer = serializer(type) as KSerializer return try { diff --git a/bson-multiplatform/src/commonMain/kotlin/BsonDocument.kt b/bson-multiplatform/src/commonMain/kotlin/BsonDocument.kt index c6af74b7..8115a6ce 100644 --- a/bson-multiplatform/src/commonMain/kotlin/BsonDocument.kt +++ b/bson-multiplatform/src/commonMain/kotlin/BsonDocument.kt @@ -23,7 +23,6 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder -import kotlinx.serialization.modules.EmptySerializersModule import kotlinx.serialization.serializer import opensavvy.ktmongo.bson.BsonDocument import opensavvy.ktmongo.bson.BsonType @@ -88,7 +87,7 @@ class BsonDocument internal constructor( @LowLevelApi override fun decode(type: KType): T { - val decoder = BsonDecoder(EmptySerializersModule(), this.asValue()) + val decoder = BsonDecoder(this.asValue()) @Suppress("UNCHECKED_CAST") return decoder.decodeSerializableValue(serializer(type) as KSerializer) } diff --git a/bson-multiplatform/src/commonMain/kotlin/BsonFactory.kt b/bson-multiplatform/src/commonMain/kotlin/BsonFactory.kt index 55e8202e..1afe6c7b 100644 --- a/bson-multiplatform/src/commonMain/kotlin/BsonFactory.kt +++ b/bson-multiplatform/src/commonMain/kotlin/BsonFactory.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, OpenSavvy and contributors. + * Copyright (c) 2025-2026, OpenSavvy and contributors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,8 @@ package opensavvy.ktmongo.bson.multiplatform import kotlinx.io.Buffer import kotlinx.io.readTo import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.modules.EmptySerializersModule +import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.serializer import opensavvy.ktmongo.bson.BsonFactory import opensavvy.ktmongo.bson.BsonFieldWriter @@ -102,7 +104,27 @@ import kotlin.time.ExperimentalTime * ``` */ @OptIn(ExperimentalTime::class) -class BsonFactory : BsonFactory { +class BsonFactory( + /** + * The [SerializersModule] used by this factory when [encoding][encode] or [decoding][opensavvy.ktmongo.bson.BsonDocument.decode] + * BSON types. + * + * You can use this module to control polymorphic and contextual serialization. + * + * - [KotlinX.Serialization documentation](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/serializers.md#serializers-module) + */ + serializersModule: SerializersModule = EmptySerializersModule(), +) : BsonFactory { + + /** + * The [SerializersModule] used by this factory when [encoding][encode] or [decoding][opensavvy.ktmongo.bson.BsonDocument.decode] + * BSON types. + * + * To customize this module, see the constructor of [opensavvy.ktmongo.bson.multiplatform.BsonFactory]. + */ + val serializersModule = SerializersModule { + include(serializersModule) + } @Suppress("NOTHING_TO_INLINE") private inline fun openArbitraryTopLevel( @@ -139,7 +161,7 @@ class BsonFactory : BsonFactory { ): Bytes { val buffer = Buffer() val bsonWriter = openArbitraryTopLevel(buffer) - MultiplatformDocumentFieldWriter(bsonWriter).block() + MultiplatformDocumentFieldWriter(this, bsonWriter).block() return closeArbitraryTopLevel(buffer, bsonWriter) } @@ -160,7 +182,7 @@ class BsonFactory : BsonFactory { val buffer = Buffer() val bsonWriter = openArbitraryTopLevel(buffer) - return object : TopCompletableBsonFieldWriter, CompletableBsonFieldWriter by MultiplatformDocumentFieldWriter(bsonWriter) { + return object : TopCompletableBsonFieldWriter, CompletableBsonFieldWriter by MultiplatformDocumentFieldWriter(this, bsonWriter) { override fun build(): BsonDocument = BsonDocument(this@BsonFactory, closeArbitraryTopLevel(buffer, bsonWriter)) } @@ -182,7 +204,7 @@ class BsonFactory : BsonFactory { val buffer = Buffer() val bsonWriter = openArbitraryTopLevel(buffer) - return object : TopCompletableBsonValueWriter, CompletableBsonValueWriter by MultiplatformArrayFieldWriter(MultiplatformDocumentFieldWriter(bsonWriter)) { + return object : TopCompletableBsonValueWriter, CompletableBsonValueWriter by MultiplatformArrayFieldWriter(MultiplatformDocumentFieldWriter(this, bsonWriter)) { override fun build(): BsonArray = BsonArray(this@BsonFactory, closeArbitraryTopLevel(buffer, bsonWriter)) } diff --git a/bson-multiplatform/src/commonMain/kotlin/BsonValue.kt b/bson-multiplatform/src/commonMain/kotlin/BsonValue.kt index 6fb1921f..6f80a6d0 100644 --- a/bson-multiplatform/src/commonMain/kotlin/BsonValue.kt +++ b/bson-multiplatform/src/commonMain/kotlin/BsonValue.kt @@ -23,7 +23,6 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder -import kotlinx.serialization.modules.EmptySerializersModule import kotlinx.serialization.serializer import opensavvy.ktmongo.bson.* import opensavvy.ktmongo.bson.BsonArray @@ -93,7 +92,7 @@ class BsonValue internal constructor( @LowLevelApi override fun decode(type: KType): T { - val decoder = BsonDecoder(EmptySerializersModule(), this) + val decoder = BsonDecoder(this) @Suppress("UNCHECKED_CAST") return decoder.decodeSerializableValue(serializer(type) as KSerializer) } diff --git a/bson-multiplatform/src/commonMain/kotlin/impl/write/MultiplatformDocumentFieldWriter.kt b/bson-multiplatform/src/commonMain/kotlin/impl/write/MultiplatformDocumentFieldWriter.kt index b94e158a..a4f10e64 100644 --- a/bson-multiplatform/src/commonMain/kotlin/impl/write/MultiplatformDocumentFieldWriter.kt +++ b/bson-multiplatform/src/commonMain/kotlin/impl/write/MultiplatformDocumentFieldWriter.kt @@ -19,12 +19,12 @@ package opensavvy.ktmongo.bson.multiplatform.impl.write import kotlinx.io.Buffer import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerializationStrategy -import kotlinx.serialization.modules.EmptySerializersModule import kotlinx.serialization.serializer import opensavvy.ktmongo.bson.BsonFieldWriter import opensavvy.ktmongo.bson.BsonType import opensavvy.ktmongo.bson.BsonValueWriter import opensavvy.ktmongo.bson.DEPRECATED_IN_BSON_SPEC +import opensavvy.ktmongo.bson.multiplatform.BsonFactory import opensavvy.ktmongo.bson.multiplatform.BsonValue import opensavvy.ktmongo.bson.multiplatform.Bytes import opensavvy.ktmongo.bson.multiplatform.RawBsonWriter @@ -37,6 +37,7 @@ import kotlin.reflect.KType @LowLevelApi internal class MultiplatformDocumentFieldWriter( + private val factory: BsonFactory, private val writer: RawBsonWriter, ) : BsonFieldWriter, CompletableBsonFieldWriter { @@ -221,7 +222,7 @@ internal class MultiplatformDocumentFieldWriter( val childBuffer = Buffer() val childWriter = RawBsonWriter(childBuffer) - writeTo(MultiplatformDocumentFieldWriter(childWriter)) + writeTo(MultiplatformDocumentFieldWriter(factory, childWriter)) closeArbitraryDocument(childBuffer, childWriter) } @@ -256,7 +257,7 @@ internal class MultiplatformDocumentFieldWriter( val childBuffer = Buffer() val childWriter = RawBsonWriter(childBuffer) - val writer = MultiplatformDocumentFieldWriter(childWriter) + val writer = MultiplatformDocumentFieldWriter(factory, childWriter) return object : CompletableBsonFieldWriter by writer { override fun complete() { this@MultiplatformDocumentFieldWriter.closeArbitraryDocument(childBuffer, childWriter) @@ -274,7 +275,7 @@ internal class MultiplatformDocumentFieldWriter( val childBuffer = Buffer() val childWriter = RawBsonWriter(childBuffer) - return object : CompletableBsonValueWriter by MultiplatformArrayFieldWriter(MultiplatformDocumentFieldWriter(childWriter)) { + return object : CompletableBsonValueWriter by MultiplatformArrayFieldWriter(MultiplatformDocumentFieldWriter(factory, childWriter)) { override fun complete() { this@MultiplatformDocumentFieldWriter.closeArbitraryDocument(childBuffer, childWriter) } @@ -291,7 +292,7 @@ internal class MultiplatformDocumentFieldWriter( @LowLevelApi override fun writeSafe(name: String, obj: T, type: KType) { val serializer = serializer(type) as SerializationStrategy - BsonEncoder(EmptySerializersModule(), open(name)).encodeSerializableValue(serializer, obj) + BsonEncoder(factory, open(name)).encodeSerializableValue(serializer, obj) } @DangerousMongoApi diff --git a/bson-multiplatform/src/commonMain/kotlin/serialization/MultiplatformDecoder.kt b/bson-multiplatform/src/commonMain/kotlin/serialization/MultiplatformDecoder.kt index 14144f29..dbf4e203 100644 --- a/bson-multiplatform/src/commonMain/kotlin/serialization/MultiplatformDecoder.kt +++ b/bson-multiplatform/src/commonMain/kotlin/serialization/MultiplatformDecoder.kt @@ -26,7 +26,6 @@ import kotlinx.serialization.descriptors.StructureKind import kotlinx.serialization.encoding.AbstractDecoder import kotlinx.serialization.encoding.CompositeDecoder import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.modules.EmptySerializersModule import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.serializer import opensavvy.ktmongo.bson.BsonDecodingException @@ -41,12 +40,14 @@ import kotlin.uuid.Uuid @ExperimentalSerializationApi internal class BsonDecoderTopLevel( - override val serializersModule: SerializersModule, private val factory: BsonFactory, val bytesWithHeader: Bytes, ) : AbstractDecoder() { var out: Any? = null + override val serializersModule: SerializersModule + get() = factory.serializersModule + override fun decodeNull(): Nothing { throw BsonEncodingException("Cannot encode a null value at the top level of BSON") } @@ -58,8 +59,8 @@ internal class BsonDecoderTopLevel( @LowLevelApi override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder { return when (descriptor.kind) { - StructureKind.CLASS, StructureKind.OBJECT -> BsonCompositeDecoder(serializersModule, BsonDocument(factory, bytesWithHeader)) - StructureKind.LIST -> BsonCompositeListDecoder(serializersModule, BsonArray(factory, bytesWithHeader)) + StructureKind.CLASS, StructureKind.OBJECT -> BsonCompositeDecoder(BsonDocument(factory, bytesWithHeader)) + StructureKind.LIST -> BsonCompositeListDecoder(BsonArray(factory, bytesWithHeader)) else -> TODO() } } @@ -72,9 +73,12 @@ internal class BsonDecoderTopLevel( @OptIn(ExperimentalTime::class, ExperimentalUuidApi::class) @LowLevelApi internal class BsonDecoder( - override val serializersModule: SerializersModule, val source: BsonValue, ) : Decoder { + + override val serializersModule: SerializersModule + get() = source.factory.serializersModule + @ExperimentalSerializationApi override fun decodeNotNullMark(): Boolean { return source.type != BsonType.Null @@ -131,8 +135,8 @@ internal class BsonDecoder( override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder { return when (descriptor.kind) { - StructureKind.CLASS, StructureKind.OBJECT -> BsonCompositeDecoder(serializersModule, source.decodeDocument()) - StructureKind.LIST -> BsonCompositeListDecoder(serializersModule, source.decodeArray()) + StructureKind.CLASS, StructureKind.OBJECT -> BsonCompositeDecoder(source.decodeDocument()) + StructureKind.LIST -> BsonCompositeListDecoder(source.decodeArray()) else -> TODO() } } @@ -181,9 +185,11 @@ internal class BsonDecoder( @LowLevelApi internal class BsonCompositeDecoder( - override val serializersModule: SerializersModule, - source: BsonDocument, + private val source: BsonDocument, ) : CompositeDecoder { + override val serializersModule: SerializersModule + get() = source.factory.serializersModule + override fun endStructure(descriptor: SerialDescriptor) { } @@ -232,25 +238,28 @@ internal class BsonCompositeDecoder( } override fun decodeInlineElement(descriptor: SerialDescriptor, index: Int): Decoder { - return BsonDecoder(serializersModule, current.value) + return BsonDecoder(current.value) } override fun decodeSerializableElement(descriptor: SerialDescriptor, index: Int, deserializer: DeserializationStrategy, previousValue: T?): T { - return BsonDecoder(serializersModule, current.value).decodeSerializableValue(deserializer) + return BsonDecoder(current.value).decodeSerializableValue(deserializer) } @ExperimentalSerializationApi override fun decodeNullableSerializableElement(descriptor: SerialDescriptor, index: Int, deserializer: DeserializationStrategy, previousValue: T?): T? { - return BsonDecoder(serializersModule, current.value).decodeNullableSerializableValue(deserializer) + return BsonDecoder(current.value).decodeNullableSerializableValue(deserializer) } } @LowLevelApi internal class BsonCompositeListDecoder( - override val serializersModule: SerializersModule, - source: BsonArray, + private val source: BsonArray, ) : CompositeDecoder { + + override val serializersModule: SerializersModule + get() = source.factory.serializersModule + override fun endStructure(descriptor: SerialDescriptor) { } @@ -300,22 +309,22 @@ internal class BsonCompositeListDecoder( } override fun decodeInlineElement(descriptor: SerialDescriptor, index: Int): Decoder { - return BsonDecoder(serializersModule, current) + return BsonDecoder(current) } override fun decodeSerializableElement(descriptor: SerialDescriptor, index: Int, deserializer: DeserializationStrategy, previousValue: T?): T { - return BsonDecoder(serializersModule, current).decodeSerializableValue(deserializer) + return BsonDecoder(current).decodeSerializableValue(deserializer) } @ExperimentalSerializationApi override fun decodeNullableSerializableElement(descriptor: SerialDescriptor, index: Int, deserializer: DeserializationStrategy, previousValue: T?): T? { - return BsonDecoder(serializersModule, current).decodeNullableSerializableValue(deserializer) + return BsonDecoder(current).decodeNullableSerializableValue(deserializer) } } @ExperimentalSerializationApi fun decodeFromBson(factory: BsonFactory, bytes: ByteArray, deserializer: DeserializationStrategy): T { - val decoder = BsonDecoderTopLevel(EmptySerializersModule(), factory, Bytes(bytes.copyOf())) + val decoder = BsonDecoderTopLevel(factory, Bytes(bytes.copyOf())) return decoder.decodeSerializableValue(deserializer) } diff --git a/bson-multiplatform/src/commonMain/kotlin/serialization/MultiplatformEncoder.kt b/bson-multiplatform/src/commonMain/kotlin/serialization/MultiplatformEncoder.kt index 27bac4a9..0ff0d2bb 100644 --- a/bson-multiplatform/src/commonMain/kotlin/serialization/MultiplatformEncoder.kt +++ b/bson-multiplatform/src/commonMain/kotlin/serialization/MultiplatformEncoder.kt @@ -25,7 +25,6 @@ import kotlinx.serialization.descriptors.StructureKind import kotlinx.serialization.encoding.AbstractEncoder import kotlinx.serialization.encoding.CompositeEncoder import kotlinx.serialization.encoding.Encoder -import kotlinx.serialization.modules.EmptySerializersModule import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.serializer import opensavvy.ktmongo.bson.multiplatform.BsonArray @@ -44,9 +43,14 @@ import kotlin.uuid.Uuid @ExperimentalSerializationApi @LowLevelApi -private class BsonEncoderTopLevel(override val serializersModule: SerializersModule, val context: BsonFactory) : AbstractEncoder() { +private class BsonEncoderTopLevel( + val factory: BsonFactory, +) : AbstractEncoder() { lateinit var out: BsonDocument + override val serializersModule: SerializersModule + get() = factory.serializersModule + override fun encodeNull() { throw BsonEncodingException("Cannot encode a null value at the top level of BSON") } @@ -59,13 +63,13 @@ private class BsonEncoderTopLevel(override val serializersModule: SerializersMod override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { val out = when (descriptor.kind) { StructureKind.CLASS, StructureKind.OBJECT -> { - val a = context.openDocument() + val a = factory.openDocument() val doc = object : CompletableBsonFieldWriter by a { override fun complete() { out = a.build() } } - BsonCompositeEncoder(serializersModule, doc) + BsonCompositeEncoder(factory, doc) } else -> TODO("Unsupported structure kind: ${descriptor.kind}") @@ -75,7 +79,13 @@ private class BsonEncoderTopLevel(override val serializersModule: SerializersMod } @OptIn(LowLevelApi::class, DangerousMongoApi::class, ExperimentalTime::class, ExperimentalUuidApi::class) -internal class BsonEncoder(override val serializersModule: SerializersModule, val out: CompletableBsonValueWriter) : Encoder { +internal class BsonEncoder( + val factory: BsonFactory, + val out: CompletableBsonValueWriter, +) : Encoder { + override val serializersModule: SerializersModule + get() = factory.serializersModule + @ExperimentalSerializationApi override fun encodeNull() { out.writeNull() @@ -127,9 +137,9 @@ internal class BsonEncoder(override val serializersModule: SerializersModule, va override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { return when (descriptor.kind) { - StructureKind.CLASS -> BsonCompositeEncoder(serializersModule, out.openDocument()) - StructureKind.LIST -> BsonCompositeEncoderList(serializersModule, out.openArray()) - StructureKind.OBJECT -> BsonCompositeEncoder(serializersModule, out.openDocument()) + StructureKind.CLASS -> BsonCompositeEncoder(factory, out.openDocument()) + StructureKind.LIST -> BsonCompositeEncoderList(factory, out.openArray()) + StructureKind.OBJECT -> BsonCompositeEncoder(factory, out.openDocument()) else -> TODO("Unsupported structure kind: ${descriptor.kind}") } } @@ -167,7 +177,13 @@ internal class BsonEncoder(override val serializersModule: SerializersModule, va @LowLevelApi @OptIn(DangerousMongoApi::class) -private class BsonCompositeEncoder(override val serializersModule: SerializersModule, val out: CompletableBsonFieldWriter) : CompositeEncoder { +private class BsonCompositeEncoder( + val factory: BsonFactory, + val out: CompletableBsonFieldWriter, +) : CompositeEncoder { + override val serializersModule: SerializersModule + get() = factory.serializersModule + override fun endStructure(descriptor: SerialDescriptor) { out.complete() } @@ -209,22 +225,28 @@ private class BsonCompositeEncoder(override val serializersModule: SerializersMo } override fun encodeInlineElement(descriptor: SerialDescriptor, index: Int): Encoder { - return BsonEncoder(serializersModule, out.open(descriptor.getElementName(index))) + return BsonEncoder(factory, out.open(descriptor.getElementName(index))) } override fun encodeSerializableElement(descriptor: SerialDescriptor, index: Int, serializer: SerializationStrategy, value: T) { - BsonEncoder(serializersModule, out.open(descriptor.getElementName(index))).encodeSerializableValue(serializer, value) + BsonEncoder(factory, out.open(descriptor.getElementName(index))).encodeSerializableValue(serializer, value) } @ExperimentalSerializationApi override fun encodeNullableSerializableElement(descriptor: SerialDescriptor, index: Int, serializer: SerializationStrategy, value: T?) { - BsonEncoder(serializersModule, out.open(descriptor.getElementName(index))).encodeNullableSerializableValue(serializer, value) + BsonEncoder(factory, out.open(descriptor.getElementName(index))).encodeNullableSerializableValue(serializer, value) } } @LowLevelApi @OptIn(DangerousMongoApi::class) -private class BsonCompositeEncoderList(override val serializersModule: SerializersModule, val out: CompletableBsonValueWriter) : CompositeEncoder { +private class BsonCompositeEncoderList( + val factory: BsonFactory, + val out: CompletableBsonValueWriter, +) : CompositeEncoder { + override val serializersModule: SerializersModule + get() = factory.serializersModule + override fun endStructure(descriptor: SerialDescriptor) { out.complete() } @@ -266,16 +288,16 @@ private class BsonCompositeEncoderList(override val serializersModule: Serialize } override fun encodeInlineElement(descriptor: SerialDescriptor, index: Int): Encoder { - return BsonEncoder(serializersModule, out) + return BsonEncoder(factory, out) } override fun encodeSerializableElement(descriptor: SerialDescriptor, index: Int, serializer: SerializationStrategy, value: T) { - BsonEncoder(serializersModule, out).encodeSerializableValue(serializer, value) + BsonEncoder(factory, out).encodeSerializableValue(serializer, value) } @ExperimentalSerializationApi override fun encodeNullableSerializableElement(descriptor: SerialDescriptor, index: Int, serializer: SerializationStrategy, value: T?) { - BsonEncoder(serializersModule, out).encodeNullableSerializableValue(serializer, value) + BsonEncoder(factory, out).encodeNullableSerializableValue(serializer, value) } } @@ -286,8 +308,8 @@ private class BsonCompositeEncoderList(override val serializersModule: Serialize */ @ExperimentalSerializationApi @OptIn(LowLevelApi::class, DangerousMongoApi::class) -fun encodeToBson(context: BsonFactory, value: T, serializer: SerializationStrategy): BsonDocument { - val encoder = BsonEncoderTopLevel(EmptySerializersModule(), context) +fun encodeToBson(factory: BsonFactory, value: T, serializer: SerializationStrategy): BsonDocument { + val encoder = BsonEncoderTopLevel(factory) encoder.encodeSerializableValue(serializer, value) return encoder.out } @@ -298,5 +320,5 @@ fun encodeToBson(context: BsonFactory, value: T, serializer: Serializa * [value] must be serializable using KotlinX.Serialization. For example, using the `@Serializable` annotation. */ @ExperimentalSerializationApi -inline fun encodeToBson(context: BsonFactory, value: T): BsonDocument = - encodeToBson(context, value, serializer()) +inline fun encodeToBson(factory: BsonFactory, value: T): BsonDocument = + encodeToBson(factory, value, serializer()) -- 2.51.2 From 3ec104cec31241f2a059d3bf48cb0adedfb79351 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Thu, 14 May 2026 14:07:33 +0200 Subject: [PATCH 09/10] refactor(bson-multiplatform): Remove the top-level serialization functions. All encoding/decoding now goes through the BsonFactory --- .../src/commonMain/kotlin/BsonFactory.kt | 9 ++++--- .../serialization/MultiplatformDecoder.kt | 11 -------- .../serialization/MultiplatformEncoder.kt | 25 +------------------ .../kotlin/serialization/EncoderTest.kt | 5 ++-- .../serialization/SerializationTestUtils.kt | 8 +++--- 5 files changed, 15 insertions(+), 43 deletions(-) diff --git a/bson-multiplatform/src/commonMain/kotlin/BsonFactory.kt b/bson-multiplatform/src/commonMain/kotlin/BsonFactory.kt index 1afe6c7b..bc758972 100644 --- a/bson-multiplatform/src/commonMain/kotlin/BsonFactory.kt +++ b/bson-multiplatform/src/commonMain/kotlin/BsonFactory.kt @@ -29,7 +29,7 @@ import opensavvy.ktmongo.bson.multiplatform.impl.write.CompletableBsonFieldWrite import opensavvy.ktmongo.bson.multiplatform.impl.write.CompletableBsonValueWriter import opensavvy.ktmongo.bson.multiplatform.impl.write.MultiplatformArrayFieldWriter import opensavvy.ktmongo.bson.multiplatform.impl.write.MultiplatformDocumentFieldWriter -import opensavvy.ktmongo.bson.multiplatform.serialization.encodeToBson +import opensavvy.ktmongo.bson.multiplatform.serialization.BsonEncoderTopLevel import opensavvy.ktmongo.dsl.DangerousMongoApi import opensavvy.ktmongo.dsl.LowLevelApi import kotlin.reflect.KType @@ -173,8 +173,11 @@ class BsonFactory( @ExperimentalSerializationApi @LowLevelApi - override fun encode(obj: T, type: KType): BsonDocument = - encodeToBson(this, obj, serializer(type)) + override fun encode(obj: T, type: KType): BsonDocument { + val encoder = BsonEncoderTopLevel(this) + encoder.encodeSerializableValue(serializer(type), obj) + return encoder.out + } @LowLevelApi @DangerousMongoApi diff --git a/bson-multiplatform/src/commonMain/kotlin/serialization/MultiplatformDecoder.kt b/bson-multiplatform/src/commonMain/kotlin/serialization/MultiplatformDecoder.kt index dbf4e203..cfa1c2be 100644 --- a/bson-multiplatform/src/commonMain/kotlin/serialization/MultiplatformDecoder.kt +++ b/bson-multiplatform/src/commonMain/kotlin/serialization/MultiplatformDecoder.kt @@ -27,7 +27,6 @@ import kotlinx.serialization.encoding.AbstractDecoder import kotlinx.serialization.encoding.CompositeDecoder import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.modules.SerializersModule -import kotlinx.serialization.serializer import opensavvy.ktmongo.bson.BsonDecodingException import opensavvy.ktmongo.bson.BsonType import opensavvy.ktmongo.bson.multiplatform.* @@ -321,13 +320,3 @@ internal class BsonCompositeListDecoder( return BsonDecoder(current).decodeNullableSerializableValue(deserializer) } } - -@ExperimentalSerializationApi -fun decodeFromBson(factory: BsonFactory, bytes: ByteArray, deserializer: DeserializationStrategy): T { - val decoder = BsonDecoderTopLevel(factory, Bytes(bytes.copyOf())) - return decoder.decodeSerializableValue(deserializer) -} - -@ExperimentalSerializationApi -inline fun decodeFromBson(factory: BsonFactory, bytes: ByteArray): T = - decodeFromBson(factory, bytes, serializer()) diff --git a/bson-multiplatform/src/commonMain/kotlin/serialization/MultiplatformEncoder.kt b/bson-multiplatform/src/commonMain/kotlin/serialization/MultiplatformEncoder.kt index 0ff0d2bb..9095253f 100644 --- a/bson-multiplatform/src/commonMain/kotlin/serialization/MultiplatformEncoder.kt +++ b/bson-multiplatform/src/commonMain/kotlin/serialization/MultiplatformEncoder.kt @@ -26,7 +26,6 @@ import kotlinx.serialization.encoding.AbstractEncoder import kotlinx.serialization.encoding.CompositeEncoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.modules.SerializersModule -import kotlinx.serialization.serializer import opensavvy.ktmongo.bson.multiplatform.BsonArray import opensavvy.ktmongo.bson.multiplatform.BsonDocument import opensavvy.ktmongo.bson.multiplatform.BsonFactory @@ -43,7 +42,7 @@ import kotlin.uuid.Uuid @ExperimentalSerializationApi @LowLevelApi -private class BsonEncoderTopLevel( +internal class BsonEncoderTopLevel( val factory: BsonFactory, ) : AbstractEncoder() { lateinit var out: BsonDocument @@ -300,25 +299,3 @@ private class BsonCompositeEncoderList( BsonEncoder(factory, out).encodeNullableSerializableValue(serializer, value) } } - -/** - * Encodes an arbitrary [value] into a [Bson] document. - * - * [value] must be serializable using KotlinX.Serialization. For example, using the `@Serializable` annotation. - */ -@ExperimentalSerializationApi -@OptIn(LowLevelApi::class, DangerousMongoApi::class) -fun encodeToBson(factory: BsonFactory, value: T, serializer: SerializationStrategy): BsonDocument { - val encoder = BsonEncoderTopLevel(factory) - encoder.encodeSerializableValue(serializer, value) - return encoder.out -} - -/** - * Encodes an arbitrary [value] into a [Bson] document. - * - * [value] must be serializable using KotlinX.Serialization. For example, using the `@Serializable` annotation. - */ -@ExperimentalSerializationApi -inline fun encodeToBson(factory: BsonFactory, value: T): BsonDocument = - encodeToBson(factory, value, serializer()) diff --git a/bson-multiplatform/src/commonTest/kotlin/serialization/EncoderTest.kt b/bson-multiplatform/src/commonTest/kotlin/serialization/EncoderTest.kt index 8198c3df..1e3a43a4 100644 --- a/bson-multiplatform/src/commonTest/kotlin/serialization/EncoderTest.kt +++ b/bson-multiplatform/src/commonTest/kotlin/serialization/EncoderTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, OpenSavvy and contributors. + * Copyright (c) 2025-2026, OpenSavvy and contributors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ package opensavvy.ktmongo.bson.multiplatform.serialization import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable +import opensavvy.ktmongo.bson.multiplatform.encode import opensavvy.ktmongo.bson.multiplatform.factory import opensavvy.ktmongo.bson.types.ObjectId import opensavvy.ktmongo.bson.types.ObjectIdGenerator @@ -36,7 +37,7 @@ val EncoderTest by preparedSuite { @Serializable data class User(val id: Int) - check(encodeToBson(factory(), User(1234)).toString() == """{"id": 1234}""") + check(factory().encode(User(1234)).toString() == """{"id": 1234}""") } suite("Complex example") { diff --git a/bson-multiplatform/src/commonTest/kotlin/serialization/SerializationTestUtils.kt b/bson-multiplatform/src/commonTest/kotlin/serialization/SerializationTestUtils.kt index 1854f17b..cd2cb197 100644 --- a/bson-multiplatform/src/commonTest/kotlin/serialization/SerializationTestUtils.kt +++ b/bson-multiplatform/src/commonTest/kotlin/serialization/SerializationTestUtils.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, OpenSavvy and contributors. + * Copyright (c) 2025-2026, OpenSavvy and contributors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,12 +19,14 @@ package opensavvy.ktmongo.bson.multiplatform.serialization import kotlinx.serialization.ExperimentalSerializationApi +import opensavvy.ktmongo.bson.decode +import opensavvy.ktmongo.bson.multiplatform.encode import opensavvy.ktmongo.bson.multiplatform.factory import opensavvy.ktmongo.dsl.LowLevelApi import opensavvy.prepared.suite.TestDsl suspend inline fun TestDsl.serializeRoundTrip(value: T) { - val ctx = factory() + val factory = factory() - check(decodeFromBson(ctx, encodeToBson(ctx, value).toByteArray()) == value) + check(factory.encode(value).decode() == value) } -- 2.51.2 From 11d51d2c426adf52b9289489007b1337a2907d21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Thu, 14 May 2026 14:12:37 +0200 Subject: [PATCH 10/10] feat(bson-multiplatform): Serialize the common BsonDocument, BsonArray and BsonValue --- .../src/commonMain/kotlin/BsonFactory.kt | 9 +++ .../CommonBsonTypeSerializers.kt | 58 +++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 bson-multiplatform/src/commonMain/kotlin/serialization/CommonBsonTypeSerializers.kt diff --git a/bson-multiplatform/src/commonMain/kotlin/BsonFactory.kt b/bson-multiplatform/src/commonMain/kotlin/BsonFactory.kt index bc758972..7a064d2d 100644 --- a/bson-multiplatform/src/commonMain/kotlin/BsonFactory.kt +++ b/bson-multiplatform/src/commonMain/kotlin/BsonFactory.kt @@ -21,6 +21,7 @@ import kotlinx.io.readTo import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.modules.EmptySerializersModule import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.modules.contextual import kotlinx.serialization.serializer import opensavvy.ktmongo.bson.BsonFactory import opensavvy.ktmongo.bson.BsonFieldWriter @@ -30,6 +31,9 @@ import opensavvy.ktmongo.bson.multiplatform.impl.write.CompletableBsonValueWrite import opensavvy.ktmongo.bson.multiplatform.impl.write.MultiplatformArrayFieldWriter import opensavvy.ktmongo.bson.multiplatform.impl.write.MultiplatformDocumentFieldWriter import opensavvy.ktmongo.bson.multiplatform.serialization.BsonEncoderTopLevel +import opensavvy.ktmongo.bson.multiplatform.serialization.CommonBsonArraySerializer +import opensavvy.ktmongo.bson.multiplatform.serialization.CommonBsonDocumentSerializer +import opensavvy.ktmongo.bson.multiplatform.serialization.CommonBsonValueSerializer import opensavvy.ktmongo.dsl.DangerousMongoApi import opensavvy.ktmongo.dsl.LowLevelApi import kotlin.reflect.KType @@ -124,6 +128,11 @@ class BsonFactory( */ val serializersModule = SerializersModule { include(serializersModule) + + // Common BsonDocument, BsonArray and BsonValue interfaces + contextual(CommonBsonDocumentSerializer) + contextual(CommonBsonArraySerializer) + contextual(CommonBsonValueSerializer) } @Suppress("NOTHING_TO_INLINE") diff --git a/bson-multiplatform/src/commonMain/kotlin/serialization/CommonBsonTypeSerializers.kt b/bson-multiplatform/src/commonMain/kotlin/serialization/CommonBsonTypeSerializers.kt new file mode 100644 index 00000000..87093dd0 --- /dev/null +++ b/bson-multiplatform/src/commonMain/kotlin/serialization/CommonBsonTypeSerializers.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2026, OpenSavvy and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package opensavvy.ktmongo.bson.multiplatform.serialization + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import opensavvy.ktmongo.bson.multiplatform.BsonArray +import opensavvy.ktmongo.bson.multiplatform.BsonDocument +import opensavvy.ktmongo.bson.multiplatform.BsonValue + +// Contextual serializers for the common interfaces (BsonDocument, BsonArray and BsonValue). +// Each interface directly delegates to the matching Multiplatform serializer. + +internal object CommonBsonDocumentSerializer : KSerializer { + override val descriptor: SerialDescriptor = BsonDocument.Serializer.descriptor + + override fun serialize(encoder: Encoder, value: opensavvy.ktmongo.bson.BsonDocument) = + encoder.encodeSerializableValue(BsonDocument.Serializer, value as BsonDocument) + + override fun deserialize(decoder: Decoder): opensavvy.ktmongo.bson.BsonDocument = + decoder.decodeSerializableValue(BsonDocument.Serializer) +} + +internal object CommonBsonArraySerializer : KSerializer { + override val descriptor: SerialDescriptor = BsonArray.Serializer.descriptor + + override fun serialize(encoder: Encoder, value: opensavvy.ktmongo.bson.BsonArray) = + encoder.encodeSerializableValue(BsonArray.Serializer, value as BsonArray) + + override fun deserialize(decoder: Decoder): opensavvy.ktmongo.bson.BsonArray = + decoder.decodeSerializableValue(BsonArray.Serializer) +} + +internal object CommonBsonValueSerializer : KSerializer { + override val descriptor: SerialDescriptor = BsonValue.Serializer.descriptor + + override fun serialize(encoder: Encoder, value: opensavvy.ktmongo.bson.BsonValue) = + encoder.encodeSerializableValue(BsonValue.Serializer, value as BsonValue) + + override fun deserialize(decoder: Decoder): opensavvy.ktmongo.bson.BsonValue = + decoder.decodeSerializableValue(BsonValue.Serializer) +} -- 2.51.2