Skip to content

Commit 9e52922

Browse files
committed
Add bsonNamingStrategy option to support snake_case naming strategy
1 parent 08880c8 commit 9e52922

File tree

8 files changed

+60
-68
lines changed

8 files changed

+60
-68
lines changed

bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonConfiguration.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,9 @@ public data class BsonConfiguration(
3131
val encodeDefaults: Boolean = true,
3232
val explicitNulls: Boolean = false,
3333
val classDiscriminator: String = "_t",
34+
val bsonNamingStrategy: BsonNamingStrategy? = null
3435
)
36+
37+
public enum class BsonNamingStrategy {
38+
SNAKE_CASE,
39+
}

bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonDecoder.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import org.bson.codecs.kotlinx.utils.BsonCodecUtils.createBsonDecoder
4242
import org.bson.codecs.kotlinx.utils.BsonCodecUtils.createBsonDocumentDecoder
4343
import org.bson.codecs.kotlinx.utils.BsonCodecUtils.createBsonMapDecoder
4444
import org.bson.codecs.kotlinx.utils.BsonCodecUtils.createBsonPolymorphicDecoder
45+
import org.bson.codecs.kotlinx.utils.BsonCodecUtils.toCamelCase
4546
import org.bson.internal.NumberCodecHelper
4647
import org.bson.internal.StringCodecHelper
4748
import org.bson.types.ObjectId
@@ -116,7 +117,14 @@ internal sealed class AbstractBsonDecoder(
116117
val elementMetadata = elementsMetadata ?: error("elementsMetadata may not be null.")
117118
val name: String? =
118119
when (reader.state ?: error("State of reader may not be null.")) {
119-
AbstractBsonReader.State.NAME -> reader.readName()
120+
AbstractBsonReader.State.NAME ->
121+
reader.readName().let {
122+
if (configuration.bsonNamingStrategy == BsonNamingStrategy.SNAKE_CASE) {
123+
it.toCamelCase
124+
} else {
125+
it
126+
}
127+
}
120128
AbstractBsonReader.State.VALUE -> reader.currentName
121129
AbstractBsonReader.State.TYPE -> {
122130
reader.readBsonType()

bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonEncoder.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import org.bson.BsonValue
3131
import org.bson.BsonWriter
3232
import org.bson.codecs.BsonValueCodec
3333
import org.bson.codecs.EncoderContext
34+
import org.bson.codecs.kotlinx.utils.BsonCodecUtils.toSnakeCase
3435
import org.bson.types.ObjectId
3536

3637
/**
@@ -203,7 +204,15 @@ internal open class BsonEncoderImpl(
203204
}
204205

205206
internal fun encodeName(value: Any) {
206-
writer.writeName(value.toString())
207+
val name =
208+
value.toString().let {
209+
if (configuration.bsonNamingStrategy == BsonNamingStrategy.SNAKE_CASE) {
210+
it.toSnakeCase
211+
} else {
212+
it
213+
}
214+
}
215+
writer.writeName(name)
207216
state = STATE.VALUE
208217
}
209218

bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/JsonBsonDecoder.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import org.bson.AbstractBsonReader
3131
import org.bson.BsonBinarySubType
3232
import org.bson.BsonType
3333
import org.bson.UuidRepresentation
34+
import org.bson.codecs.kotlinx.utils.BsonCodecUtils.toJsonNamingStrategy
3435
import org.bson.internal.UuidHelper
3536

3637
@OptIn(ExperimentalSerializationApi::class)
@@ -42,6 +43,7 @@ internal interface JsonBsonDecoder : BsonDecoder, JsonDecoder {
4243
explicitNulls = configuration.explicitNulls
4344
encodeDefaults = configuration.encodeDefaults
4445
classDiscriminator = configuration.classDiscriminator
46+
namingStrategy = configuration.bsonNamingStrategy.toJsonNamingStrategy()
4547
serializersModule = this@JsonBsonDecoder.serializersModule
4648
}
4749

bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/JsonBsonEncoder.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import kotlinx.serialization.json.int
3030
import kotlinx.serialization.json.long
3131
import kotlinx.serialization.modules.SerializersModule
3232
import org.bson.BsonWriter
33+
import org.bson.codecs.kotlinx.utils.BsonCodecUtils.toJsonNamingStrategy
3334
import org.bson.types.Decimal128
3435

3536
@OptIn(ExperimentalSerializationApi::class)
@@ -52,6 +53,7 @@ internal class JsonBsonEncoder(
5253
explicitNulls = configuration.explicitNulls
5354
encodeDefaults = configuration.encodeDefaults
5455
classDiscriminator = configuration.classDiscriminator
56+
namingStrategy = configuration.bsonNamingStrategy.toJsonNamingStrategy()
5557
serializersModule = this@JsonBsonEncoder.serializersModule
5658
}
5759

bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/utils/BsonCodecUtils.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515
*/
1616
package org.bson.codecs.kotlinx.utils
1717

18+
import java.util.*
1819
import kotlinx.serialization.ExperimentalSerializationApi
1920
import kotlinx.serialization.descriptors.SerialDescriptor
21+
import kotlinx.serialization.json.JsonNamingStrategy
2022
import kotlinx.serialization.modules.SerializersModule
2123
import org.bson.AbstractBsonReader
2224
import org.bson.BsonWriter
@@ -28,6 +30,7 @@ import org.bson.codecs.kotlinx.BsonDocumentDecoder
2830
import org.bson.codecs.kotlinx.BsonEncoder
2931
import org.bson.codecs.kotlinx.BsonEncoderImpl
3032
import org.bson.codecs.kotlinx.BsonMapDecoder
33+
import org.bson.codecs.kotlinx.BsonNamingStrategy
3134
import org.bson.codecs.kotlinx.BsonPolymorphicDecoder
3235
import org.bson.codecs.kotlinx.JsonBsonArrayDecoder
3336
import org.bson.codecs.kotlinx.JsonBsonDecoderImpl
@@ -116,4 +119,17 @@ internal object BsonCodecUtils {
116119
return if (hasJsonDecoder) JsonBsonMapDecoder(descriptor, reader, serializersModule, configuration)
117120
else BsonMapDecoder(descriptor, reader, serializersModule, configuration)
118121
}
122+
123+
internal val String.toSnakeCase
124+
get() = replace(Regex("([A-Z])"), "_$1").lowercase(Locale.getDefault()).replace(Regex("^_"), "")
125+
126+
internal val String.toCamelCase
127+
get() = replace(Regex("_([a-z])")) { it.value[1].uppercaseChar().toString() }
128+
129+
internal fun BsonNamingStrategy?.toJsonNamingStrategy(): JsonNamingStrategy? {
130+
return when (this) {
131+
BsonNamingStrategy.SNAKE_CASE -> JsonNamingStrategy.SnakeCase
132+
else -> null
133+
}
134+
}
119135
}

bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/KotlinSerializerCodecTest.kt

Lines changed: 10 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,7 @@ import kotlinx.datetime.LocalTime
2626
import kotlinx.serialization.ExperimentalSerializationApi
2727
import kotlinx.serialization.MissingFieldException
2828
import kotlinx.serialization.SerializationException
29-
import kotlinx.serialization.json.JsonPrimitive
30-
import kotlinx.serialization.json.buildJsonArray
31-
import kotlinx.serialization.json.buildJsonObject
32-
import kotlinx.serialization.json.put
29+
import kotlinx.serialization.json.*
3330
import kotlinx.serialization.modules.SerializersModule
3431
import kotlinx.serialization.modules.plus
3532
import kotlinx.serialization.modules.polymorphic
@@ -49,68 +46,7 @@ import org.bson.BsonUndefined
4946
import org.bson.codecs.DecoderContext
5047
import org.bson.codecs.EncoderContext
5148
import org.bson.codecs.configuration.CodecConfigurationException
52-
import org.bson.codecs.kotlinx.samples.Box
53-
import org.bson.codecs.kotlinx.samples.DataClassBsonValues
54-
import org.bson.codecs.kotlinx.samples.DataClassContainsOpen
55-
import org.bson.codecs.kotlinx.samples.DataClassContainsValueClass
56-
import org.bson.codecs.kotlinx.samples.DataClassEmbedded
57-
import org.bson.codecs.kotlinx.samples.DataClassKey
58-
import org.bson.codecs.kotlinx.samples.DataClassLastItemDefaultsToNull
59-
import org.bson.codecs.kotlinx.samples.DataClassListOfDataClasses
60-
import org.bson.codecs.kotlinx.samples.DataClassListOfListOfDataClasses
61-
import org.bson.codecs.kotlinx.samples.DataClassListOfSealed
62-
import org.bson.codecs.kotlinx.samples.DataClassMapOfDataClasses
63-
import org.bson.codecs.kotlinx.samples.DataClassMapOfListOfDataClasses
64-
import org.bson.codecs.kotlinx.samples.DataClassNestedParameterizedTypes
65-
import org.bson.codecs.kotlinx.samples.DataClassOpen
66-
import org.bson.codecs.kotlinx.samples.DataClassOpenA
67-
import org.bson.codecs.kotlinx.samples.DataClassOpenB
68-
import org.bson.codecs.kotlinx.samples.DataClassOptionalBsonValues
69-
import org.bson.codecs.kotlinx.samples.DataClassParameterized
70-
import org.bson.codecs.kotlinx.samples.DataClassSealed
71-
import org.bson.codecs.kotlinx.samples.DataClassSealedA
72-
import org.bson.codecs.kotlinx.samples.DataClassSealedB
73-
import org.bson.codecs.kotlinx.samples.DataClassSealedC
74-
import org.bson.codecs.kotlinx.samples.DataClassSelfReferential
75-
import org.bson.codecs.kotlinx.samples.DataClassWithAnnotations
76-
import org.bson.codecs.kotlinx.samples.DataClassWithBooleanMapKey
77-
import org.bson.codecs.kotlinx.samples.DataClassWithBsonConstructor
78-
import org.bson.codecs.kotlinx.samples.DataClassWithBsonDiscriminator
79-
import org.bson.codecs.kotlinx.samples.DataClassWithBsonExtraElements
80-
import org.bson.codecs.kotlinx.samples.DataClassWithBsonId
81-
import org.bson.codecs.kotlinx.samples.DataClassWithBsonIgnore
82-
import org.bson.codecs.kotlinx.samples.DataClassWithBsonProperty
83-
import org.bson.codecs.kotlinx.samples.DataClassWithBsonRepresentation
84-
import org.bson.codecs.kotlinx.samples.DataClassWithCollections
85-
import org.bson.codecs.kotlinx.samples.DataClassWithContextualDateValues
86-
import org.bson.codecs.kotlinx.samples.DataClassWithDataClassMapKey
87-
import org.bson.codecs.kotlinx.samples.DataClassWithDateValues
88-
import org.bson.codecs.kotlinx.samples.DataClassWithDefaults
89-
import org.bson.codecs.kotlinx.samples.DataClassWithEmbedded
90-
import org.bson.codecs.kotlinx.samples.DataClassWithEncodeDefault
91-
import org.bson.codecs.kotlinx.samples.DataClassWithEnum
92-
import org.bson.codecs.kotlinx.samples.DataClassWithEnumMapKey
93-
import org.bson.codecs.kotlinx.samples.DataClassWithFailingInit
94-
import org.bson.codecs.kotlinx.samples.DataClassWithJsonElement
95-
import org.bson.codecs.kotlinx.samples.DataClassWithJsonElements
96-
import org.bson.codecs.kotlinx.samples.DataClassWithJsonElementsNullable
97-
import org.bson.codecs.kotlinx.samples.DataClassWithListThatLastItemDefaultsToNull
98-
import org.bson.codecs.kotlinx.samples.DataClassWithMutableList
99-
import org.bson.codecs.kotlinx.samples.DataClassWithMutableMap
100-
import org.bson.codecs.kotlinx.samples.DataClassWithMutableSet
101-
import org.bson.codecs.kotlinx.samples.DataClassWithNestedParameterized
102-
import org.bson.codecs.kotlinx.samples.DataClassWithNestedParameterizedDataClass
103-
import org.bson.codecs.kotlinx.samples.DataClassWithNullableGeneric
104-
import org.bson.codecs.kotlinx.samples.DataClassWithNulls
105-
import org.bson.codecs.kotlinx.samples.DataClassWithPair
106-
import org.bson.codecs.kotlinx.samples.DataClassWithParameterizedDataClass
107-
import org.bson.codecs.kotlinx.samples.DataClassWithRequired
108-
import org.bson.codecs.kotlinx.samples.DataClassWithSequence
109-
import org.bson.codecs.kotlinx.samples.DataClassWithSimpleValues
110-
import org.bson.codecs.kotlinx.samples.DataClassWithTriple
111-
import org.bson.codecs.kotlinx.samples.Key
112-
import org.bson.codecs.kotlinx.samples.SealedInterface
113-
import org.bson.codecs.kotlinx.samples.ValueClass
49+
import org.bson.codecs.kotlinx.samples.*
11450
import org.bson.json.JsonMode
11551
import org.bson.json.JsonWriterSettings
11652
import org.junit.jupiter.api.Test
@@ -1126,6 +1062,13 @@ class KotlinSerializerCodecTest {
11261062
}
11271063
}
11281064

1065+
@Test
1066+
fun testSnakeCaseNamingStrategy() {
1067+
val expected = """{"camel_case_key": "snake_case_value", "a_b_cd": "camelCaseValue"}"""
1068+
val dataClass = DataClassWithCamelCase("snake_case_value", "camelCaseValue")
1069+
assertRoundTrips(expected, dataClass, BsonConfiguration(bsonNamingStrategy = BsonNamingStrategy.SNAKE_CASE))
1070+
}
1071+
11291072
private inline fun <reified T : Any> assertRoundTrips(
11301073
expected: String,
11311074
value: T,
@@ -1184,6 +1127,7 @@ class KotlinSerializerCodecTest {
11841127
serializersModule: SerializersModule = defaultSerializersModule,
11851128
configuration: BsonConfiguration = BsonConfiguration()
11861129
): T {
1130+
println("Deserializing: ${value.toJson()}")
11871131
val codec = KotlinSerializerCodec.create(T::class, serializersModule, configuration)!!
11881132
return codec.decode(BsonDocumentReader(value), DecoderContext.builder().build())
11891133
}

bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/samples/DataClasses.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,12 @@ data class DataClassWithDefaults(
102102
val listSimple: List<String> = listOf("a", "b", "c")
103103
)
104104

105+
@Serializable
106+
data class DataClassWithCamelCase(
107+
val camelCaseKey: String,
108+
val aBCd: String,
109+
)
110+
105111
@Serializable data class DataClassWithNulls(val boolean: Boolean?, val string: String?, val listSimple: List<String?>?)
106112

107113
@Serializable

0 commit comments

Comments
 (0)