diff --git a/formats/hocon/api/kotlinx-serialization-hocon.api b/formats/hocon/api/kotlinx-serialization-hocon.api index 4afe9d3cc0..e114392ec8 100644 --- a/formats/hocon/api/kotlinx-serialization-hocon.api +++ b/formats/hocon/api/kotlinx-serialization-hocon.api @@ -35,6 +35,15 @@ public final class kotlinx/serialization/hocon/HoconKt { public static synthetic fun Hocon$default (Lkotlinx/serialization/hocon/Hocon;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/serialization/hocon/Hocon; } +public abstract interface annotation class kotlinx/serialization/hocon/HoconName : java/lang/annotation/Annotation { + public abstract fun value ()Ljava/lang/String; +} + +public synthetic class kotlinx/serialization/hocon/HoconName$Impl : kotlinx/serialization/hocon/HoconName { + public fun (Ljava/lang/String;)V + public final synthetic fun value ()Ljava/lang/String; +} + public final class kotlinx/serialization/hocon/serializers/ConfigMemorySizeSerializer : kotlinx/serialization/KSerializer { public static final field INSTANCE Lkotlinx/serialization/hocon/serializers/ConfigMemorySizeSerializer; public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/typesafe/config/ConfigMemorySize; diff --git a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconAnnotations.kt b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconAnnotations.kt new file mode 100644 index 0000000000..cf1030c9d0 --- /dev/null +++ b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconAnnotations.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2017-2025 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.serialization.hocon + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerialInfo +import kotlinx.serialization.SerialName + +/** + * This annotation has a higher priority than [SerialName] or [Hocon.useConfigNamingConvention]. + * This means that you have full control over property name for encoding and decoding in the HOCON format in practice. + */ +@SerialInfo +@Target(AnnotationTarget.PROPERTY) +@ExperimentalSerializationApi +public annotation class HoconName(val value: String) diff --git a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/NamingConvention.kt b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/NamingConvention.kt index 4071bc7bc8..0eaf8edf2c 100644 --- a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/NamingConvention.kt +++ b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/NamingConvention.kt @@ -7,6 +7,10 @@ private val NAMING_CONVENTION_REGEX by lazy { "[A-Z]".toRegex() } @OptIn(ExperimentalSerializationApi::class) internal fun SerialDescriptor.getConventionElementName(index: Int, useConfigNamingConvention: Boolean): String { + val hoconName = getElementAnnotations(index).firstOrNull { it is HoconName } as HoconName? + if (hoconName != null) { + return hoconName.value + } val originalName = getElementName(index) return if (!useConfigNamingConvention) originalName else originalName.replace(NAMING_CONVENTION_REGEX) { "-${it.value.lowercase()}" } diff --git a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconNamingConventionTest.kt b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconNamingConventionTest.kt index 889abcd07a..4803691134 100644 --- a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconNamingConventionTest.kt +++ b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconNamingConventionTest.kt @@ -14,10 +14,10 @@ class HoconNamingConventionTest { data class CaseConfig(val aCharValue: Char, val aStringValue: String) @Serializable - data class SerialNameConfig(@SerialName("an-id-value") val anIDValue: Int) + data class HoconNameConfig(@HoconName("anID-value") val anIDValue: Int) @Serializable - data class CaseWithInnerConfig(val caseConfig: CaseConfig, val serialNameConfig: SerialNameConfig) + data class CaseWithInnerConfig(val caseConfig: CaseConfig, val hoconNameConfig: HoconNameConfig) private val hocon = Hocon { useConfigNamingConvention = true @@ -39,42 +39,42 @@ class HoconNamingConventionTest { } @Test - fun testDeserializeUsingSerialNameInsteadOfNamingConvention() { - val obj = deserializeConfig("an-id-value = 42", SerialNameConfig.serializer(), true) + fun testDeserializeUsingHoconNameInsteadOfNamingConvention() { + val obj = deserializeConfig("anID-value = 42", HoconNameConfig.serializer(), true) assertEquals(42, obj.anIDValue) } @Test - fun testSerializeUsingSerialNameInsteadOfNamingConvention() { - val obj = SerialNameConfig(anIDValue = 42) + fun testSerializeUsingHoconNameInsteadOfNamingConvention() { + val obj = HoconNameConfig(anIDValue = 42) val config = hocon.encodeToConfig(obj) - config.assertContains("an-id-value = 42") + config.assertContains("anID-value = 42") } @Test fun testDeserializeInnerValuesUsingNamingConvention() { - val configString = "case-config {a-char-value = b, a-string-value = bar}, serial-name-config {an-id-value = 21}" + val configString = "case-config {a-char-value = b, a-string-value = bar}, hocon-name-config {anID-value = 21}" val obj = deserializeConfig(configString, CaseWithInnerConfig.serializer(), true) with(obj.caseConfig) { assertEquals('b', aCharValue) assertEquals("bar", aStringValue) } - assertEquals(21, obj.serialNameConfig.anIDValue) + assertEquals(21, obj.hoconNameConfig.anIDValue) } @Test fun testSerializeInnerValuesUsingNamingConvention() { val obj = CaseWithInnerConfig( caseConfig = CaseConfig(aCharValue = 't', aStringValue = "test"), - serialNameConfig = SerialNameConfig(anIDValue = 42) + hoconNameConfig = HoconNameConfig(anIDValue = 42) ) val config = hocon.encodeToConfig(obj) config.assertContains( """ case-config { a-char-value = t, a-string-value = test } - serial-name-config { an-id-value = 42 } + hocon-name-config { anID-value = 42 } """ ) }