io.quarkus
quarkus-junit5-internal
diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization-common/runtime/src/main/java/io/quarkus/resteasy/reactive/kotlin/serialization/common/runtime/JsonConfig.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization-common/runtime/src/main/java/io/quarkus/resteasy/reactive/kotlin/serialization/common/runtime/JsonConfig.java
index 7631e7e0a8e4f..4655bd1b1da86 100644
--- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization-common/runtime/src/main/java/io/quarkus/resteasy/reactive/kotlin/serialization/common/runtime/JsonConfig.java
+++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization-common/runtime/src/main/java/io/quarkus/resteasy/reactive/kotlin/serialization/common/runtime/JsonConfig.java
@@ -1,5 +1,6 @@
package io.quarkus.resteasy.reactive.kotlin.serialization.common.runtime;
+import java.util.Optional;
import java.util.StringJoiner;
import io.quarkus.runtime.annotations.ConfigGroup;
@@ -100,6 +101,28 @@ public class JsonConfig {
@ConfigItem(defaultValue = "false")
public boolean useArrayPolymorphism = false;
+ /**
+ * Specifies the {@code JsonNamingStrategy} that should be used for all properties in classes for serialization and
+ * deserialization.
+ * This strategy is applied for all entities that have {@code StructureKind.CLASS}.
+ *
+ *
+ * {@code null} by default.
+ *
+ *
+ * This element can be one of two things:
+ *
+ * - the fully qualified class name of a type implements the {@code NamingStrategy} interface and has a no-arg
+ * constructor
+ * - a value in the form {@code NamingStrategy.SnakeCase} which refers to built-in values provided by the kotlin
+ * serialization
+ * library itself.
+ *
+ *
+ */
+ @ConfigItem(name = "naming-strategy")
+ public Optional namingStrategy;
+
@Override
public String toString() {
return new StringJoiner(", ", JsonConfig.class.getSimpleName() + "[", "]")
diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization-common/runtime/src/main/kotlin/io/quarkus/resteasy/reactive/kotlin/serialization/common/runtime/JsonProducer.kt b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization-common/runtime/src/main/kotlin/io/quarkus/resteasy/reactive/kotlin/serialization/common/runtime/JsonProducer.kt
index 09c24b4b80235..b6abedc87305b 100644
--- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization-common/runtime/src/main/kotlin/io/quarkus/resteasy/reactive/kotlin/serialization/common/runtime/JsonProducer.kt
+++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization-common/runtime/src/main/kotlin/io/quarkus/resteasy/reactive/kotlin/serialization/common/runtime/JsonProducer.kt
@@ -7,10 +7,16 @@ import jakarta.enterprise.inject.Produces
import jakarta.inject.Singleton
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.JsonBuilder
+import kotlinx.serialization.json.JsonNamingStrategy
+import kotlinx.serialization.json.JsonNamingStrategy.Builtins
+import java.lang.Thread
+import kotlin.reflect.KMutableProperty1
+import kotlin.reflect.full.memberProperties
+import kotlin.reflect.jvm.isAccessible
@Singleton
class JsonProducer {
-
@ExperimentalSerializationApi
@Singleton
@Produces
@@ -29,12 +35,65 @@ class JsonProducer {
useAlternativeNames = configuration.json.useAlternativeNames
useArrayPolymorphism = configuration.json.useArrayPolymorphism
+ configuration.json.namingStrategy.ifPresent { strategy ->
+ loadStrategy(this, strategy, this@JsonProducer)
+ }
val sortedCustomizers = sortCustomizersInDescendingPriorityOrder(customizers)
for (customizer in sortedCustomizers) {
customizer.customize(this)
}
}
+ @ExperimentalSerializationApi
+ private fun loadStrategy(
+ jsonBuilder: JsonBuilder,
+ strategy: String,
+ jsonProducer: JsonProducer
+ ) {
+ val strategyProperty: KMutableProperty1 = (
+ JsonBuilder::class.memberProperties
+ .find { member -> member.name == "namingStrategy" }
+ ?: throw ReflectiveOperationException("Could not find the namingStrategy property on JsonBuilder")
+ ) as KMutableProperty1
+ strategyProperty.isAccessible = true
+
+ strategyProperty.set(
+ jsonBuilder,
+ if (strategy.startsWith("JsonNamingStrategy")) {
+ jsonProducer.extractBuiltIn(strategy)
+ } else {
+ jsonProducer.loadStrategyClass(strategy)
+ }
+ )
+ }
+
+ @ExperimentalSerializationApi
+ private fun loadStrategyClass(
+ strategy: String
+ ): JsonNamingStrategy {
+ try {
+ val strategyClass: Class = Thread.currentThread().contextClassLoader.loadClass(strategy) as Class
+ val constructor = strategyClass.constructors
+ .find { it.parameterCount == 0 }
+ ?: throw ReflectiveOperationException("No no-arg constructor found on $strategy")
+ return constructor.newInstance() as JsonNamingStrategy
+ } catch (e: ReflectiveOperationException) {
+ throw IllegalArgumentException("Error loading naming strategy: ${strategy.substringAfter('.')}", e)
+ }
+ }
+
+ @ExperimentalSerializationApi
+ private fun extractBuiltIn(
+ strategy: String
+ ): JsonNamingStrategy {
+ val kClass = Builtins::class
+ val property = kClass.memberProperties.find { property ->
+ property.name == strategy.substringAfter('.')
+ } ?: throw IllegalArgumentException("Unknown naming strategy provided: ${strategy.substringAfter('.')}")
+
+ return property.get(JsonNamingStrategy) as JsonNamingStrategy
+ }
+
private fun sortCustomizersInDescendingPriorityOrder(
customizers: Iterable
): List {
diff --git a/integration-tests/kotlin-serialization/src/main/kotlin/io/quarkus/it/kotser/GreetingResource.kt b/integration-tests/kotlin-serialization/src/main/kotlin/io/quarkus/it/kotser/GreetingResource.kt
index 5e7bf549507f3..28a4d209cff58 100644
--- a/integration-tests/kotlin-serialization/src/main/kotlin/io/quarkus/it/kotser/GreetingResource.kt
+++ b/integration-tests/kotlin-serialization/src/main/kotlin/io/quarkus/it/kotser/GreetingResource.kt
@@ -62,7 +62,7 @@ class GreetingResource {
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
fun marry(person: Person): Person {
- return Person(person.name.substringBefore(" ") + " Halpert")
+ return Person(person.fullName.substringBefore(" ") + " Halpert")
}
@GET
diff --git a/integration-tests/kotlin-serialization/src/main/kotlin/io/quarkus/it/kotser/TitleCase.kt b/integration-tests/kotlin-serialization/src/main/kotlin/io/quarkus/it/kotser/TitleCase.kt
new file mode 100644
index 0000000000000..50b30a3d95645
--- /dev/null
+++ b/integration-tests/kotlin-serialization/src/main/kotlin/io/quarkus/it/kotser/TitleCase.kt
@@ -0,0 +1,12 @@
+package io.quarkus.it.kotser
+
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.json.JsonNamingStrategy
+
+@OptIn(ExperimentalSerializationApi::class)
+class TitleCase : JsonNamingStrategy {
+ override fun serialNameForJson(descriptor: SerialDescriptor, elementIndex: Int, serialName: String): String {
+ return serialName[0].uppercase() + serialName.substring(1)
+ }
+}
diff --git a/integration-tests/kotlin-serialization/src/main/kotlin/io/quarkus/it/kotser/model/Person.kt b/integration-tests/kotlin-serialization/src/main/kotlin/io/quarkus/it/kotser/model/Person.kt
index 07411157a9b21..b36d1ad8f7ab8 100644
--- a/integration-tests/kotlin-serialization/src/main/kotlin/io/quarkus/it/kotser/model/Person.kt
+++ b/integration-tests/kotlin-serialization/src/main/kotlin/io/quarkus/it/kotser/model/Person.kt
@@ -3,7 +3,7 @@ package io.quarkus.it.kotser.model
import kotlinx.serialization.Serializable
@Serializable
-data class Person(var name: String, var defaulted: String = "hi there!") {
+data class Person(var fullName: String, var defaulted: String = "hi there!") {
override fun toString(): String {
TODO("this shouldn't get called. a proper serialization should be invoked.")
}
diff --git a/integration-tests/kotlin-serialization/src/main/kotlin/io/quarkus/it/kotser/model/Person2.kt b/integration-tests/kotlin-serialization/src/main/kotlin/io/quarkus/it/kotser/model/Person2.kt
index a958d2771e4d2..b5e3718e1d4f9 100644
--- a/integration-tests/kotlin-serialization/src/main/kotlin/io/quarkus/it/kotser/model/Person2.kt
+++ b/integration-tests/kotlin-serialization/src/main/kotlin/io/quarkus/it/kotser/model/Person2.kt
@@ -3,7 +3,7 @@ package io.quarkus.it.kotser.model
import kotlinx.serialization.Serializable
@Serializable
-data class Person2(var name: String, var defaulted: String = "hey") {
+data class Person2(var fullName: String, var defaulted: String = "hey") {
override fun toString(): String {
TODO("this shouldn't get called. a proper serialization should be invoked.")
}
diff --git a/integration-tests/kotlin-serialization/src/main/resources/application.properties b/integration-tests/kotlin-serialization/src/main/resources/application.properties
index 4dadb74e15205..cbe86cde035ca 100644
--- a/integration-tests/kotlin-serialization/src/main/resources/application.properties
+++ b/integration-tests/kotlin-serialization/src/main/resources/application.properties
@@ -1 +1,3 @@
-quarkus.kotlin-serialization.json.encode-defaults=true
\ No newline at end of file
+quarkus.kotlin-serialization.json.encode-defaults=true
+#quarkus.kotlin-serialization.json.naming-strategy=io.quarkus.it.kotser.TitleCase
+#quarkus.kotlin-serialization.json.naming-strategy=JsonNamingStrategy.SnakeCase
\ No newline at end of file
diff --git a/integration-tests/kotlin-serialization/src/test/kotlin/io/quarkus/it/kotser/ResourceTest.kt b/integration-tests/kotlin-serialization/src/test/kotlin/io/quarkus/it/kotser/ResourceTest.kt
index a9a19e4a701b5..cb14dd4913d1e 100644
--- a/integration-tests/kotlin-serialization/src/test/kotlin/io/quarkus/it/kotser/ResourceTest.kt
+++ b/integration-tests/kotlin-serialization/src/test/kotlin/io/quarkus/it/kotser/ResourceTest.kt
@@ -9,16 +9,36 @@ import jakarta.ws.rs.core.MediaType
import org.hamcrest.CoreMatchers
import org.hamcrest.CoreMatchers.`is`
import org.junit.jupiter.api.Test
+import java.util.Properties
@QuarkusTest
open class ResourceTest {
+
+ val nameField: String
+ var defaulted = "defaulted"
+
+ init {
+ val properties = Properties()
+ properties.load(javaClass.getResourceAsStream("/application.properties"))
+ val strategy: String? = properties.get("quarkus.kotlin-serialization.json.naming-strategy") as String?
+ when (strategy) {
+ "JsonNamingStrategy.SnakeCase" -> nameField = "full_name"
+ TitleCase::class.qualifiedName -> {
+ nameField = "FullName"
+ defaulted = "Defaulted"
+ }
+ null -> nameField = "fullName"
+ else -> throw IllegalArgumentException("unknown strategy: $strategy")
+ }
+ }
+
@Test
fun testGetFlow() {
When {
get("/flow")
} Then {
statusCode(200)
- body(`is`("""[{"name":"Jim Halpert","defaulted":"hi there!"}]"""))
+ body(`is`("""[{"$nameField":"Jim Halpert","$defaulted":"hi there!"}]"""))
}
}
@@ -28,7 +48,7 @@ open class ResourceTest {
get("/")
} Then {
statusCode(200)
- body(`is`("""{"name":"Jim Halpert","defaulted":"hi there!"}"""))
+ body(`is`("""{"$nameField":"Jim Halpert","$defaulted":"hi there!"}"""))
}
}
@@ -38,7 +58,7 @@ open class ResourceTest {
get("/restResponse")
} Then {
statusCode(200)
- body(`is`("""{"name":"Jim Halpert","defaulted":"hi there!"}"""))
+ body(`is`("""{"$nameField":"Jim Halpert","$defaulted":"hi there!"}"""))
}
}
@@ -48,7 +68,7 @@ open class ResourceTest {
get("/restResponseList")
} Then {
statusCode(200)
- body(`is`("""[{"name":"Jim Halpert","defaulted":"hi there!"}]"""))
+ body(`is`("""[{"$nameField":"Jim Halpert","$defaulted":"hi there!"}]"""))
}
}
@@ -58,7 +78,7 @@ open class ResourceTest {
get("/unknownType")
} Then {
statusCode(200)
- body(`is`("""{"name":"Foo Bar","defaulted":"hey"}"""))
+ body(`is`("""{"$nameField":"Foo Bar","$defaulted":"hey"}"""))
}
}
@@ -68,7 +88,7 @@ open class ResourceTest {
get("/suspend")
} Then {
statusCode(200)
- body(`is`("""{"name":"Jim Halpert","defaulted":"hi there!"}"""))
+ body(`is`("""{"$nameField":"Jim Halpert","$defaulted":"hi there!"}"""))
}
}
@@ -78,20 +98,20 @@ open class ResourceTest {
get("/suspendList")
} Then {
statusCode(200)
- body(`is`("""[{"name":"Jim Halpert","defaulted":"hi there!"}]"""))
+ body(`is`("""[{"$nameField":"Jim Halpert","$defaulted":"hi there!"}]"""))
}
}
@Test
fun testPost() {
Given {
- body("""{ "name": "Pam Beasley" }""")
+ body("""{ "$nameField": "Pam Beasley" }""")
contentType(JSON)
} When {
post("/")
} Then {
statusCode(200)
- body(`is`("""{"name":"Pam Halpert","defaulted":"hi there!"}"""))
+ body(`is`("""{"$nameField":"Pam Halpert","$defaulted":"hi there!"}"""))
}
}