diff --git a/bson-kotlinx/build.gradle.kts b/bson-kotlinx/build.gradle.kts index bb9dd42e10b..f98469172cb 100644 --- a/bson-kotlinx/build.gradle.kts +++ b/bson-kotlinx/build.gradle.kts @@ -45,6 +45,7 @@ dependencies { implementation(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.5.0")) implementation("org.jetbrains.kotlinx:kotlinx-serialization-core") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json") api(project(path = ":bson", configuration = "default")) implementation("org.jetbrains.kotlin:kotlin-reflect") diff --git a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonDecoder.kt b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonDecoder.kt index b4cbad3b9dd..0e46dc4211f 100644 --- a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonDecoder.kt +++ b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonDecoder.kt @@ -27,15 +27,28 @@ import kotlinx.serialization.encoding.AbstractDecoder import kotlinx.serialization.encoding.CompositeDecoder import kotlinx.serialization.encoding.CompositeDecoder.Companion.DECODE_DONE import kotlinx.serialization.encoding.CompositeDecoder.Companion.UNKNOWN_NAME +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.modules.SerializersModule import org.bson.AbstractBsonReader +import org.bson.BsonBinarySubType import org.bson.BsonInvalidOperationException import org.bson.BsonReader import org.bson.BsonType import org.bson.BsonValue +import org.bson.UuidRepresentation import org.bson.codecs.BsonValueCodec import org.bson.codecs.DecoderContext +import org.bson.internal.UuidHelper import org.bson.types.ObjectId +import java.util.Base64 /** * The BsonDecoder interface @@ -58,7 +71,14 @@ internal open class DefaultBsonDecoder( internal val reader: AbstractBsonReader, override val serializersModule: SerializersModule, internal val configuration: BsonConfiguration -) : BsonDecoder, AbstractDecoder() { +) : BsonDecoder, JsonDecoder, AbstractDecoder() { + + override val json = Json { + explicitNulls = configuration.explicitNulls + encodeDefaults = configuration.encodeDefaults + classDiscriminator = configuration.classDiscriminator + serializersModule = this@DefaultBsonDecoder.serializersModule + } private data class ElementMetadata(val name: String, val nullable: Boolean, var processed: Boolean = false) private var elementsMetadata: Array? = null @@ -178,8 +198,91 @@ internal open class DefaultBsonDecoder( override fun decodeObjectId(): ObjectId = readOrThrow({ reader.readObjectId() }, BsonType.OBJECT_ID) override fun decodeBsonValue(): BsonValue = bsonValueCodec.decode(reader, DecoderContext.builder().build()) + + @Suppress("ComplexMethod") + override fun decodeJsonElement(): JsonElement = reader.run { + + if (state == AbstractBsonReader.State.INITIAL || + state == AbstractBsonReader.State.SCOPE_DOCUMENT || + state == AbstractBsonReader.State.TYPE) { + readBsonType() + } + + if (state == AbstractBsonReader.State.NAME) { + // ignore name + skipName() + } + + // @formatter:off + return when (currentBsonType) { + BsonType.DOCUMENT -> readJsonObject() + BsonType.ARRAY -> readJsonArray() + BsonType.NULL -> JsonPrimitive(decodeNull()) + BsonType.STRING -> JsonPrimitive(decodeString()) + BsonType.BOOLEAN -> JsonPrimitive(decodeBoolean()) + BsonType.INT32 -> JsonPrimitive(decodeInt()) + BsonType.INT64 -> JsonPrimitive(decodeLong()) + BsonType.DOUBLE -> JsonPrimitive(decodeDouble()) + BsonType.DECIMAL128 -> JsonPrimitive(reader.readDecimal128()) + BsonType.OBJECT_ID -> JsonPrimitive(decodeObjectId().toHexString()) + BsonType.DATE_TIME -> JsonPrimitive(reader.readDateTime()) + BsonType.TIMESTAMP -> JsonPrimitive(reader.readTimestamp().value) + BsonType.BINARY -> { + val subtype = reader.peekBinarySubType() + val data = reader.readBinaryData().data + when (subtype) { + BsonBinarySubType.UUID_LEGACY.value -> JsonPrimitive( + UuidHelper.decodeBinaryToUuid( + data, subtype, + UuidRepresentation.JAVA_LEGACY + ).toString() + ) + BsonBinarySubType.UUID_STANDARD.value -> JsonPrimitive( + UuidHelper.decodeBinaryToUuid( + data, subtype, + UuidRepresentation.STANDARD + ).toString() + ) + else -> JsonPrimitive(Base64.getEncoder().encodeToString(data)) + } + } + else -> error("unsupported json type: $currentBsonType") + } + // @formatter:on + } + override fun reader(): BsonReader = reader + private fun readJsonObject(): JsonObject { + + reader.readStartDocument() + val obj = buildJsonObject { + var type = reader.readBsonType() + while (type != BsonType.END_OF_DOCUMENT) { + put(reader.readName(), decodeJsonElement()) + type = reader.readBsonType() + } + } + + reader.readEndDocument() + return obj + } + + private fun readJsonArray(): JsonArray { + + reader.readStartArray() + val array = buildJsonArray { + var type = reader.readBsonType() + while (type != BsonType.END_OF_DOCUMENT) { + add(decodeJsonElement()) + type = reader.readBsonType() + } + } + + reader.readEndArray() + return array + } + private inline fun readOrThrow(action: () -> T, bsonType: BsonType): T { return try { action() diff --git a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonEncoder.kt b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonEncoder.kt index 2e68b992700..9de6a5fd4e0 100644 --- a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonEncoder.kt +++ b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonEncoder.kt @@ -25,12 +25,24 @@ import kotlinx.serialization.descriptors.SerialKind import kotlinx.serialization.descriptors.StructureKind import kotlinx.serialization.encoding.AbstractEncoder import kotlinx.serialization.encoding.CompositeEncoder +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonEncoder +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.double +import kotlinx.serialization.json.int +import kotlinx.serialization.json.long import kotlinx.serialization.modules.SerializersModule import org.bson.BsonValue import org.bson.BsonWriter import org.bson.codecs.BsonValueCodec import org.bson.codecs.EncoderContext +import org.bson.types.Decimal128 import org.bson.types.ObjectId +import java.math.BigDecimal /** * The BsonEncoder interface @@ -62,17 +74,29 @@ internal class DefaultBsonEncoder( private val writer: BsonWriter, override val serializersModule: SerializersModule, private val configuration: BsonConfiguration -) : BsonEncoder, AbstractEncoder() { +) : BsonEncoder, JsonEncoder, AbstractEncoder() { companion object { val validKeyKinds = setOf(PrimitiveKind.STRING, PrimitiveKind.CHAR, SerialKind.ENUM) val bsonValueCodec = BsonValueCodec() + private val DOUBLE_MIN_VALUE = BigDecimal.valueOf(Double.MIN_VALUE) + private val DOUBLE_MAX_VALUE = BigDecimal.valueOf(Double.MAX_VALUE) + private val INT_MIN_VALUE = BigDecimal.valueOf(Int.MIN_VALUE.toLong()) + private val INT_MAX_VALUE = BigDecimal.valueOf(Int.MAX_VALUE.toLong()) + private val LONG_MIN_VALUE = BigDecimal.valueOf(Long.MIN_VALUE) + private val LONG_MAX_VALUE = BigDecimal.valueOf(Long.MAX_VALUE) } private var isPolymorphic = false private var state = STATE.VALUE private var mapState = MapState() private var deferredElementName: String? = null + override val json = Json { + explicitNulls = configuration.explicitNulls + encodeDefaults = configuration.encodeDefaults + classDiscriminator = configuration.classDiscriminator + serializersModule = this@DefaultBsonEncoder.serializersModule + } override fun shouldEncodeElementDefault(descriptor: SerialDescriptor, index: Int): Boolean = configuration.encodeDefaults @@ -143,10 +167,10 @@ internal class DefaultBsonEncoder( deferredElementName?.let { if (value != null || configuration.explicitNulls) { encodeName(it) - super.encodeNullableSerializableValue(serializer, value) + super.encodeNullableSerializableValue(serializer, value) } } - ?: super.encodeNullableSerializableValue(serializer, value) + ?: super.encodeNullableSerializableValue(serializer, value) } override fun encodeByte(value: Byte) = encodeInt(value.toInt()) @@ -183,8 +207,55 @@ internal class DefaultBsonEncoder( bsonValueCodec.encode(writer, value, EncoderContext.builder().build()) } + override fun encodeJsonElement(element: JsonElement) = when(element) { + is JsonNull -> encodeNull() + is JsonPrimitive -> encodeJsonPrimitive(element) + is JsonObject -> encodeJsonObject(element) + is JsonArray -> encodeJsonArray(element) + } + override fun writer(): BsonWriter = writer + private fun encodeJsonPrimitive(primitive: JsonPrimitive) { + val content = primitive.content + when { + primitive.isString -> encodeString(content) + content == "true" || content == "false" -> + encodeBoolean(content.toBooleanStrict()) + else -> { + val decimal = BigDecimal(content) + when { + decimal.stripTrailingZeros().scale() > 0 -> + if (DOUBLE_MIN_VALUE <= decimal && decimal <= DOUBLE_MAX_VALUE) { + encodeDouble(primitive.double) + } else { + writer.writeDecimal128(Decimal128(decimal)) + } + INT_MIN_VALUE <= decimal && decimal <= INT_MAX_VALUE -> + encodeInt(primitive.int) + LONG_MIN_VALUE <= decimal && decimal <= LONG_MAX_VALUE -> + encodeLong(primitive.long) + else -> writer.writeDecimal128(Decimal128(decimal)) + } + } + } + } + + private fun encodeJsonObject(obj: JsonObject) { + writer.writeStartDocument() + obj.forEach { k, v -> + writer.writeName(k) + encodeJsonElement(v) + } + writer.writeEndDocument() + } + + private fun encodeJsonArray(array: JsonArray) { + writer.writeStartArray() + array.forEach(::encodeJsonElement) + writer.writeEndArray() + } + private fun encodeName(value: Any) { writer.writeName(value.toString()) deferredElementName = null diff --git a/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/KotlinSerializerCodecTest.kt b/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/KotlinSerializerCodecTest.kt index 146e897c59b..ea63ea6caa4 100644 --- a/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/KotlinSerializerCodecTest.kt +++ b/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/KotlinSerializerCodecTest.kt @@ -19,6 +19,8 @@ import kotlin.test.assertEquals import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.MissingFieldException import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.plus import kotlinx.serialization.modules.polymorphic @@ -71,6 +73,7 @@ import org.bson.codecs.kotlinx.samples.DataClassWithEncodeDefault import org.bson.codecs.kotlinx.samples.DataClassWithEnum import org.bson.codecs.kotlinx.samples.DataClassWithEnumMapKey import org.bson.codecs.kotlinx.samples.DataClassWithFailingInit +import org.bson.codecs.kotlinx.samples.DataClassWithJsonElement import org.bson.codecs.kotlinx.samples.DataClassWithMutableList import org.bson.codecs.kotlinx.samples.DataClassWithMutableMap import org.bson.codecs.kotlinx.samples.DataClassWithMutableSet @@ -129,6 +132,46 @@ class KotlinSerializerCodecTest { private val allBsonTypesDocument = BsonDocument.parse(allBsonTypesJson) + @Test + fun testDataClassWithJsonElement() { + + /* + * We need to encode all integer values as longs because the JsonElementSerializer + * doesn't actually use our JsonEncoder instead it uses an inferior + * JsonPrimitiveSerializer and ignores ours altogether encoding all integers as longs + * + * On the other hand, BsonDocument decodes everything as integers unless it's explicitly + * set as a long, and therefore we get a type mismatch + */ + + val expected = """{"value": { + |"char": "c", + |"byte": {"$numberLong": "0"}, + |"short": {"$numberLong": "1"}, + |"int": {"$numberLong": "22"}, + |"long": {"$numberLong": "42"}, + |"float": 4.0, + |"double": 4.2, + |"boolean": true, + |"string": "String" + |}}""".trimMargin() + val dataClass = DataClassWithJsonElement( + buildJsonObject { + put("char", "c") + put("byte", 0) + put("short", 1) + put("int", 22) + put("long", 42L) + put("float", 4.0) + put("double", 4.2) + put("boolean", true) + put("string", "String") + } + ) + + assertRoundTrips(expected, dataClass) + } + @Test fun testDataClassWithSimpleValues() { val expected = diff --git a/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/samples/DataClasses.kt b/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/samples/DataClasses.kt index ea5e3fea3cd..1b897a535c0 100644 --- a/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/samples/DataClasses.kt +++ b/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/samples/DataClasses.kt @@ -21,6 +21,7 @@ import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Required import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonElement import org.bson.BsonArray import org.bson.BsonBinary import org.bson.BsonBoolean @@ -50,6 +51,11 @@ import org.bson.codecs.pojo.annotations.BsonProperty import org.bson.codecs.pojo.annotations.BsonRepresentation import org.bson.types.ObjectId +@Serializable +data class DataClassWithJsonElement( + val value: JsonElement +) + @Serializable data class DataClassWithSimpleValues( val char: Char,