Skip to content

Commit d271407

Browse files
authored
Add support for object polymorhism in HOCON decoder (#1136)
Fixes #764
1 parent 5a8f09f commit d271407

File tree

3 files changed

+216
-13
lines changed

3 files changed

+216
-13
lines changed

formats/hocon/api/kotlinx-serialization-hocon.api

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
public abstract class kotlinx/serialization/hocon/Hocon : kotlinx/serialization/SerialFormat {
22
public static final field Default Lkotlinx/serialization/hocon/Hocon$Default;
3-
public synthetic fun <init> (ZLkotlinx/serialization/modules/SerializersModule;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
3+
public synthetic fun <init> (ZZLjava/lang/String;Lkotlinx/serialization/modules/SerializersModule;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
44
public final fun decodeFromConfig (Lkotlinx/serialization/DeserializationStrategy;Lcom/typesafe/config/Config;)Ljava/lang/Object;
55
public fun getSerializersModule ()Lkotlinx/serialization/modules/SerializersModule;
66
}
@@ -9,9 +9,13 @@ public final class kotlinx/serialization/hocon/Hocon$Default : kotlinx/serializa
99
}
1010

1111
public final class kotlinx/serialization/hocon/HoconBuilder {
12+
public final fun getClassDiscriminator ()Ljava/lang/String;
1213
public final fun getSerializersModule ()Lkotlinx/serialization/modules/SerializersModule;
14+
public final fun getUseArrayPolymorphism ()Z
1315
public final fun getUseConfigNamingConvention ()Z
16+
public final fun setClassDiscriminator (Ljava/lang/String;)V
1417
public final fun setSerializersModule (Lkotlinx/serialization/modules/SerializersModule;)V
18+
public final fun setUseArrayPolymorphism (Z)V
1519
public final fun setUseConfigNamingConvention (Z)V
1620
}
1721

formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@ import kotlinx.serialization.modules.*
2525
*/
2626
@ExperimentalSerializationApi
2727
public sealed class Hocon(
28-
internal val useConfigNamingConvention: Boolean,
29-
override val serializersModule: SerializersModule
28+
internal val useConfigNamingConvention: Boolean,
29+
internal val useArrayPolymorphism: Boolean,
30+
internal val classDiscriminator: String,
31+
override val serializersModule: SerializersModule
3032
) : SerialFormat {
3133

3234
@ExperimentalSerializationApi
@@ -37,7 +39,7 @@ public sealed class Hocon(
3739
* The default instance of Hocon parser.
3840
*/
3941
@ExperimentalSerializationApi
40-
public companion object Default : Hocon(false, EmptySerializersModule) {
42+
public companion object Default : Hocon(false, false, "type", EmptySerializersModule) {
4143
private val NAMING_CONVENTION_REGEX by lazy { "[A-Z]".toRegex() }
4244
}
4345

@@ -119,15 +121,44 @@ public sealed class Hocon(
119121
return decodeTaggedNotNullMark(currentTag)
120122
}
121123

122-
override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder =
123-
when {
124-
descriptor.kind.listLike -> ListConfigReader(conf.getList(currentTag))
125-
descriptor.kind.objLike -> if (ind > -1) ConfigReader(conf.getConfig(currentTag)) else this
126-
descriptor.kind == StructureKind.MAP ->
124+
override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): T {
125+
if (deserializer !is AbstractPolymorphicSerializer<*> || useArrayPolymorphism) {
126+
return deserializer.deserialize(this)
127+
}
128+
129+
val config = if (currentTagOrNull != null) conf.getConfig(currentTag) else conf
130+
131+
val reader = ConfigReader(config)
132+
val type = reader.decodeTaggedString(classDiscriminator)
133+
val actualSerializer = deserializer.findPolymorphicSerializerOrNull(reader, type)
134+
?: throwSerializerNotFound(type)
135+
136+
@Suppress("UNCHECKED_CAST")
137+
return (actualSerializer as DeserializationStrategy<T>).deserialize(reader)
138+
}
139+
140+
private fun throwSerializerNotFound(type: String?): Nothing {
141+
val suffix = if (type == null) "missing class discriminator ('null')" else "class discriminator '$type'"
142+
throw SerializationException("Polymorphic serializer was not found for $suffix")
143+
}
144+
145+
override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder {
146+
val kind = when (descriptor.kind) {
147+
is PolymorphicKind -> {
148+
if (useArrayPolymorphism) StructureKind.LIST else StructureKind.MAP
149+
}
150+
else -> descriptor.kind
151+
}
152+
153+
return when {
154+
kind.listLike -> ListConfigReader(conf.getList(currentTag))
155+
kind.objLike -> if (ind > -1) ConfigReader(conf.getConfig(currentTag)) else this
156+
kind == StructureKind.MAP ->
127157
// if current tag is null - map in the root of config
128158
MapConfigReader(if (currentTagOrNull != null) conf.getObject(currentTag) else conf.root())
129159
else -> this
130160
}
161+
}
131162
}
132163

133164
private inner class ListConfigReader(private val list: ConfigList) : ConfigConverter<Int>() {
@@ -216,7 +247,7 @@ public inline fun <reified T> Hocon.decodeFromConfig(config: Config): T =
216247
public fun Hocon(from: Hocon = Hocon, builderAction: HoconBuilder.() -> Unit): Hocon {
217248
val builder = HoconBuilder(from)
218249
builder.builderAction()
219-
return HoconImpl(builder.useConfigNamingConvention, builder.serializersModule)
250+
return HoconImpl(builder.useConfigNamingConvention, builder.useArrayPolymorphism, builder.classDiscriminator, builder.serializersModule)
220251
}
221252

222253
/**
@@ -233,10 +264,25 @@ public class HoconBuilder internal constructor(hocon: Hocon) {
233264
* Switches naming resolution to config naming convention: hyphen separated.
234265
*/
235266
public var useConfigNamingConvention: Boolean = hocon.useConfigNamingConvention
267+
268+
/**
269+
* Switches polymorphic serialization to the default array format.
270+
* This is an option for legacy polymorphism format and should not be generally used.
271+
* `false` by default.
272+
*/
273+
public var useArrayPolymorphism: Boolean = hocon.useArrayPolymorphism
274+
275+
/**
276+
* Name of the class descriptor property for polymorphic serialization.
277+
* "type" by default.
278+
*/
279+
public var classDiscriminator: String = hocon.classDiscriminator
236280
}
237281

238282
@OptIn(ExperimentalSerializationApi::class)
239283
private class HoconImpl(
240-
useConfigNamingConvention: Boolean = false,
241-
serializersModule: SerializersModule = EmptySerializersModule
242-
): Hocon(useConfigNamingConvention, serializersModule)
284+
useConfigNamingConvention: Boolean,
285+
useArrayPolymorphism: Boolean,
286+
classDiscriminator: String,
287+
serializersModule: SerializersModule
288+
) : Hocon(useConfigNamingConvention, useArrayPolymorphism, classDiscriminator, serializersModule)
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
package kotlinx.serialization.hocon
2+
3+
import com.typesafe.config.ConfigFactory
4+
import kotlinx.serialization.*
5+
import org.junit.Assert.*
6+
import org.junit.Test
7+
8+
class HoconPolymorphismTest {
9+
@Serializable
10+
sealed class Sealed(val intField: Int) {
11+
@Serializable
12+
@SerialName("object")
13+
object ObjectChild : Sealed(0)
14+
15+
@Serializable
16+
@SerialName("data_class")
17+
data class DataClassChild(val name: String) : Sealed(1)
18+
19+
@Serializable
20+
@SerialName("type_child")
21+
data class TypeChild(val type: String) : Sealed(2)
22+
23+
@Serializable
24+
@SerialName("annotated_type_child")
25+
data class AnnotatedTypeChild(@SerialName("my_type") val type: String) : Sealed(3)
26+
}
27+
28+
@Serializable
29+
data class CompositeClass(var sealed: Sealed)
30+
31+
32+
private val arrayHocon = Hocon {
33+
useArrayPolymorphism = true
34+
}
35+
36+
private val objectHocon = Hocon {
37+
useArrayPolymorphism = false
38+
}
39+
40+
41+
@Test
42+
fun testArrayDataClass() {
43+
val config = ConfigFactory.parseString(
44+
"""{
45+
sealed: [
46+
"data_class"
47+
{name="testArrayDataClass"
48+
intField=10}
49+
]
50+
}""")
51+
val root = arrayHocon.decodeFromConfig(CompositeClass.serializer(), config)
52+
val sealed = root.sealed
53+
54+
assertTrue(sealed is Sealed.DataClassChild)
55+
sealed as Sealed.DataClassChild
56+
assertEquals("testArrayDataClass", sealed.name)
57+
assertEquals(10, sealed.intField)
58+
}
59+
60+
@Test
61+
fun testArrayObject() {
62+
val config = ConfigFactory.parseString(
63+
"""{
64+
sealed: [
65+
"object"
66+
{}
67+
]
68+
}""")
69+
val root = arrayHocon.decodeFromConfig(CompositeClass.serializer(), config)
70+
val sealed = root.sealed
71+
72+
assertSame(Sealed.ObjectChild, sealed)
73+
}
74+
75+
@Test
76+
fun testObject() {
77+
val config = ConfigFactory.parseString("""{type="object"}""")
78+
val sealed = objectHocon.decodeFromConfig(Sealed.serializer(), config)
79+
80+
assertSame(Sealed.ObjectChild, sealed)
81+
}
82+
83+
@Test
84+
fun testNestedDataClass() {
85+
val config = ConfigFactory.parseString(
86+
"""{
87+
sealed: {
88+
type="data_class"
89+
name="test name"
90+
intField=10
91+
}
92+
}""")
93+
val root = objectHocon.decodeFromConfig(CompositeClass.serializer(), config)
94+
val sealed = root.sealed
95+
96+
assertTrue(sealed is Sealed.DataClassChild)
97+
sealed as Sealed.DataClassChild
98+
assertEquals("test name", sealed.name)
99+
assertEquals(10, sealed.intField)
100+
}
101+
102+
@Test
103+
fun testDataClass() {
104+
val config = ConfigFactory.parseString(
105+
"""{
106+
type="data_class"
107+
name="testDataClass"
108+
intField=10
109+
}""")
110+
val sealed = objectHocon.decodeFromConfig(Sealed.serializer(), config)
111+
112+
assertTrue(sealed is Sealed.DataClassChild)
113+
sealed as Sealed.DataClassChild
114+
assertEquals("testDataClass", sealed.name)
115+
assertEquals(10, sealed.intField)
116+
}
117+
118+
@Test
119+
fun testChangeDiscriminator() {
120+
val hocon = Hocon(objectHocon) {
121+
classDiscriminator = "key"
122+
}
123+
124+
val config = ConfigFactory.parseString(
125+
"""{
126+
type="override"
127+
key="type_child"
128+
intField=11
129+
}""")
130+
val sealed = hocon.decodeFromConfig(Sealed.serializer(), config)
131+
132+
assertTrue(sealed is Sealed.TypeChild)
133+
sealed as Sealed.TypeChild
134+
assertEquals("override", sealed.type)
135+
assertEquals(11, sealed.intField)
136+
}
137+
138+
@Test
139+
fun testChangeTypePropertyName() {
140+
val config = ConfigFactory.parseString(
141+
"""{
142+
my_type="override"
143+
type="annotated_type_child"
144+
intField=12
145+
}""")
146+
val sealed = objectHocon.decodeFromConfig(Sealed.serializer(), config)
147+
148+
assertTrue(sealed is Sealed.AnnotatedTypeChild)
149+
sealed as Sealed.AnnotatedTypeChild
150+
assertEquals("override", sealed.type)
151+
assertEquals(12, sealed.intField)
152+
}
153+
}

0 commit comments

Comments
 (0)