diff --git a/src/main/kotlin/com/charleskorn/kaml/Yaml.kt b/src/main/kotlin/com/charleskorn/kaml/Yaml.kt index 750bd8c0..a45bbffa 100644 --- a/src/main/kotlin/com/charleskorn/kaml/Yaml.kt +++ b/src/main/kotlin/com/charleskorn/kaml/Yaml.kt @@ -48,7 +48,7 @@ class Yaml( override fun flush() { } } - val output = YamlOutput(writer, configuration) + val output = YamlOutput(writer, this) output.encode(serializer, obj) return writer.toString() diff --git a/src/main/kotlin/com/charleskorn/kaml/YamlInput.kt b/src/main/kotlin/com/charleskorn/kaml/YamlInput.kt index b16a8abf..c49af586 100644 --- a/src/main/kotlin/com/charleskorn/kaml/YamlInput.kt +++ b/src/main/kotlin/com/charleskorn/kaml/YamlInput.kt @@ -36,6 +36,7 @@ sealed class YamlInput(val node: YamlNode, override var context: SerialModule, v is YamlNull -> YamlNullInput(node, context, configuration) is YamlList -> YamlListInput(node, context, configuration) is YamlMap -> YamlMapInput(node, context, configuration) + is YamlTaggedNode -> YamlTaggedInput(node, context, configuration) } } @@ -214,7 +215,7 @@ private class YamlMapInput(val map: YamlMap, context: SerialModule, configuratio private fun getPropertyName(key: YamlNode): String = when (key) { is YamlScalar -> key.content - is YamlNull, is YamlMap, is YamlList -> throw MalformedYamlException("Property name must not be a list, map or null value. (To use 'null' as a property name, enclose it in quotes.)", key.location) + is YamlNull, is YamlMap, is YamlList, is YamlTaggedNode -> throw MalformedYamlException("Property name must not be a list, map, null or tagged value. (To use 'null' as a property name, enclose it in quotes.)", key.location) } private fun throwUnknownProperty(name: String, location: Location, desc: SerialDescriptor): Nothing { @@ -289,3 +290,49 @@ private class YamlMapInput(val map: YamlMap, context: SerialModule, configuratio override fun getCurrentLocation(): Location = currentValueDecoder.node.location } + +private class YamlTaggedInput(val taggedNode: YamlTaggedNode, context: SerialModule, configuration: YamlConfiguration) : YamlInput(taggedNode, context, configuration) { + /** + * index 0 -> tag + * index 1 -> child node + */ + private var currentIndex = -1 + private val childDecoder: YamlInput = createFor(taggedNode.node, context, configuration) + + override fun getCurrentLocation(): Location = if (currentIndex == 1) childDecoder.getCurrentLocation() else taggedNode.location + + override fun decodeNotNullMark(): Boolean = when (currentIndex) { + 0 -> true + 1 -> childDecoder.decodeNotNullMark() + else -> super.decodeNotNullMark() + } + + override fun decodeElementIndex(desc: SerialDescriptor): Int { + return when (++currentIndex) { + 0, 1 -> currentIndex + else -> READ_DONE + } + } + + override fun decodeString(): String { + return when (currentIndex) { + 0 -> taggedNode.tag + 1 -> childDecoder.decodeString() + else -> super.decodeString() + } + } + + override fun decodeNull(): Nothing? = if (currentIndex == 1) childDecoder.decodeNull() else super.decodeNull() + override fun decodeUnit() = if (currentIndex == 1) childDecoder.decodeUnit() else super.decodeUnit() + override fun decodeInt(): Int = if (currentIndex == 1) childDecoder.decodeInt() else super.decodeInt() + override fun decodeLong(): Long = if (currentIndex == 1) childDecoder.decodeLong() else super.decodeLong() + override fun decodeShort(): Short = if (currentIndex == 1) childDecoder.decodeShort() else super.decodeShort() + override fun decodeByte(): Byte = if (currentIndex == 1) childDecoder.decodeByte() else super.decodeByte() + override fun decodeDouble(): Double = if (currentIndex == 1) childDecoder.decodeDouble() else super.decodeDouble() + override fun decodeFloat(): Float = if (currentIndex == 1) childDecoder.decodeFloat() else super.decodeFloat() + override fun decodeBoolean(): Boolean = if (currentIndex == 1) childDecoder.decodeBoolean() else super.decodeBoolean() + override fun decodeChar(): Char = if (currentIndex == 1) childDecoder.decodeChar() else super.decodeChar() + override fun decodeEnum(enumDescription: EnumDescriptor): Int = if (currentIndex == 1) childDecoder.decodeEnum(enumDescription) else super.decodeEnum(enumDescription) + override fun beginStructure(desc: SerialDescriptor, vararg typeParams: KSerializer<*>): CompositeDecoder = + if (currentIndex == 1) childDecoder.beginStructure(desc, *typeParams) else super.beginStructure(desc, *typeParams) +} diff --git a/src/main/kotlin/com/charleskorn/kaml/YamlNode.kt b/src/main/kotlin/com/charleskorn/kaml/YamlNode.kt index 704c9ff6..74ac9b91 100644 --- a/src/main/kotlin/com/charleskorn/kaml/YamlNode.kt +++ b/src/main/kotlin/com/charleskorn/kaml/YamlNode.kt @@ -143,3 +143,19 @@ data class YamlMap(val entries: Map, override val location: override fun contentToString(): String = "{" + entries.map { (key, value) -> "${key.contentToString()}: ${value.contentToString()}" }.joinToString(", ") + "}" } + +data class YamlTaggedNode(val tag: String, val node: YamlNode) : YamlNode(node.location) { + override fun equivalentContentTo(other: YamlNode): Boolean { + if (other !is YamlTaggedNode) { + return false + } + + if (tag != other.tag) { + return false + } + + return node.equivalentContentTo(other.node) + } + + override fun contentToString(): String = "!$tag ${node.contentToString()}" +} diff --git a/src/main/kotlin/com/charleskorn/kaml/YamlNodeReader.kt b/src/main/kotlin/com/charleskorn/kaml/YamlNodeReader.kt index 06c1a46c..7098c8cb 100644 --- a/src/main/kotlin/com/charleskorn/kaml/YamlNodeReader.kt +++ b/src/main/kotlin/com/charleskorn/kaml/YamlNodeReader.kt @@ -25,6 +25,7 @@ import org.snakeyaml.engine.v1.events.MappingStartEvent import org.snakeyaml.engine.v1.events.NodeEvent import org.snakeyaml.engine.v1.events.ScalarEvent import org.snakeyaml.engine.v1.events.SequenceStartEvent +import java.util.Optional class YamlNodeReader( private val parser: YamlParser, @@ -52,9 +53,9 @@ class YamlNodeReader( } private fun readFromEvent(event: Event, isTopLevel: Boolean): YamlNode = when (event) { - is ScalarEvent -> readScalarOrNull(event) - is SequenceStartEvent -> readSequence(event.location) - is MappingStartEvent -> readMapping(event.location, isTopLevel) + is ScalarEvent -> readScalarOrNull(event).maybeToTaggedNode(event.tag) + is SequenceStartEvent -> readSequence(event.location).maybeToTaggedNode(event.tag) + is MappingStartEvent -> readMapping(event.location, isTopLevel).maybeToTaggedNode(event.tag) is AliasEvent -> readAlias(event) else -> throw MalformedYamlException("Unexpected ${event.eventId}", event.location) } @@ -112,6 +113,9 @@ class YamlNodeReader( } } + private fun YamlNode.maybeToTaggedNode(tag: Optional): YamlNode = + tag.map { YamlTaggedNode(it, this) }.orElse(this) + private fun YamlNode.isScalarAndStartsWith(prefix: String): Boolean = this is YamlScalar && this.content.startsWith(prefix) private fun doMerges(items: Map): Map { diff --git a/src/main/kotlin/com/charleskorn/kaml/YamlOutput.kt b/src/main/kotlin/com/charleskorn/kaml/YamlOutput.kt index 7541a3a0..dfcf8188 100644 --- a/src/main/kotlin/com/charleskorn/kaml/YamlOutput.kt +++ b/src/main/kotlin/com/charleskorn/kaml/YamlOutput.kt @@ -21,9 +21,12 @@ package com.charleskorn.kaml import kotlinx.serialization.CompositeEncoder import kotlinx.serialization.ElementValueEncoder import kotlinx.serialization.KSerializer +import kotlinx.serialization.PolymorphicSerializer import kotlinx.serialization.SerialDescriptor +import kotlinx.serialization.SerializationStrategy import kotlinx.serialization.StructureKind import kotlinx.serialization.internal.EnumDescriptor +import kotlinx.serialization.modules.SerialModule import org.snakeyaml.engine.v1.api.DumpSettingsBuilder import org.snakeyaml.engine.v1.api.StreamDataWriter import org.snakeyaml.engine.v1.common.FlowStyle @@ -41,10 +44,19 @@ import java.util.Optional internal class YamlOutput( writer: StreamDataWriter, - private val configuration: YamlConfiguration + private val yaml: Yaml ) : ElementValueEncoder() { private val settings = DumpSettingsBuilder().build() private val emitter = Emitter(settings, writer) + private var currentTag: String? = null + private val configuration: YamlConfiguration get() = yaml.configuration + override val context: SerialModule get() = yaml.context + private val yamlTag: Optional + get() { + return Optional.ofNullable(currentTag).also { + currentTag = null + } + } init { emitter.emit(StreamStartEvent()) @@ -75,10 +87,24 @@ internal class YamlOutput( return super.encodeElement(desc, index) } + override fun encodeSerializableValue(serializer: SerializationStrategy, value: T) { + if (serializer !is PolymorphicSerializer<*>) { + serializer.serialize(this, value) + return + } + + @Suppress("UNCHECKED_CAST") + val actualSerializer = serializer.findPolymorphicSerializer(this, value as Any) as KSerializer + currentTag = actualSerializer.descriptor.name + actualSerializer.serialize(this, value) + } + override fun beginStructure(desc: SerialDescriptor, vararg typeParams: KSerializer<*>): CompositeEncoder { + val tag: Optional = yamlTag + val implicit = tag.isEmpty when (desc.kind) { - is StructureKind.LIST -> emitter.emit(SequenceStartEvent(Optional.empty(), Optional.empty(), true, FlowStyle.BLOCK)) - is StructureKind.MAP, StructureKind.CLASS -> emitter.emit(MappingStartEvent(Optional.empty(), Optional.empty(), true, FlowStyle.BLOCK)) + is StructureKind.LIST -> emitter.emit(SequenceStartEvent(Optional.empty(), tag, implicit, FlowStyle.BLOCK)) + is StructureKind.MAP, StructureKind.CLASS -> emitter.emit(MappingStartEvent(Optional.empty(), tag, implicit, FlowStyle.BLOCK)) } return super.beginStructure(desc, *typeParams) @@ -94,5 +120,9 @@ internal class YamlOutput( } private fun emitScalar(value: String, style: ScalarStyle) = - emitter.emit(ScalarEvent(Optional.empty(), Optional.empty(), ImplicitTuple(true, true), value, style)) + emitter.emit(ScalarEvent(Optional.empty(), yamlTag, ALL_IMPLICIT, value, style)) + + companion object { + private val ALL_IMPLICIT = ImplicitTuple(true, true) + } } diff --git a/src/test/kotlin/com/charleskorn/kaml/YamlReadingTest.kt b/src/test/kotlin/com/charleskorn/kaml/YamlReadingTest.kt index 9dbf357d..37b8642d 100644 --- a/src/test/kotlin/com/charleskorn/kaml/YamlReadingTest.kt +++ b/src/test/kotlin/com/charleskorn/kaml/YamlReadingTest.kt @@ -22,10 +22,17 @@ import ch.tutteli.atrium.api.cc.en_GB.message import ch.tutteli.atrium.api.cc.en_GB.toBe import ch.tutteli.atrium.api.cc.en_GB.toThrow import ch.tutteli.atrium.verbs.assert +import com.charleskorn.kaml.testobjects.InterfaceInt +import com.charleskorn.kaml.testobjects.InterfaceString +import com.charleskorn.kaml.testobjects.InterfaceWrapper import com.charleskorn.kaml.testobjects.NestedObjects +import com.charleskorn.kaml.testobjects.SealedWrapper import com.charleskorn.kaml.testobjects.SimpleStructure import com.charleskorn.kaml.testobjects.Team import com.charleskorn.kaml.testobjects.TestEnum +import com.charleskorn.kaml.testobjects.TestSealedStructure +import com.charleskorn.kaml.testobjects.interfaceModule +import com.charleskorn.kaml.testobjects.sealedModule import kotlinx.serialization.ContextualSerialization import kotlinx.serialization.Decoder import kotlinx.serialization.Encoder @@ -831,6 +838,120 @@ object YamlReadingTest : Spek({ } } + val sealedYaml = Yaml(context = sealedModule) + + context("given some tagged input representing an object where the resulting type should be a sealed class (int)") { + val input = """ + element: ! + value: 3 + """.trimIndent() + + context("parsing that input") { + val result = sealedYaml.parse(SealedWrapper.serializer(), input) + it("deserializes it to a Kotlin object") { + assert(result).toBe(SealedWrapper(TestSealedStructure.SimpleSealedInt(3))) + } + } + } + + context("given some tagged input representing an object where the resulting type should be a sealed class (string)") { + val input = """ + element: ! + value: "asdfg" + """.trimIndent() + + context("parsing that input") { + val result = sealedYaml.parse(SealedWrapper.serializer(), input) + it("deserializes it to a Kotlin object") { + assert(result).toBe(SealedWrapper(TestSealedStructure.SimpleSealedString("asdfg"))) + } + } + } + + context("given some tagged input representing a list of objects where the resulting type should be a sealed class") { + val input = """ + - element: ! + value: "some" + - element: ! + value: -987 + - element: ! + value: 654 + - element: ! + value: "tests" + """.trimIndent() + + context("parsing that input") { + val result = sealedYaml.parse(SealedWrapper.serializer().list, input) + it("deserializes it to a Kotlin object") { + assert(result).toBe( + listOf( + SealedWrapper(TestSealedStructure.SimpleSealedString("some")), + SealedWrapper(TestSealedStructure.SimpleSealedInt(-987)), + SealedWrapper(TestSealedStructure.SimpleSealedInt(654)), + SealedWrapper(TestSealedStructure.SimpleSealedString("tests")) + ) + ) + } + } + } + + val interfaceYaml = Yaml(context = interfaceModule) + + context("given some tagged input representing an object where the resulting type should be an interface (int)") { + val input = """ + test: ! + intVal: 55 + """.trimIndent() + + context("parsing that input") { + val result = interfaceYaml.parse(InterfaceWrapper.serializer(), input) + it("deserializes it to a Kotlin object") { + assert(result).toBe(InterfaceWrapper(InterfaceInt(55))) + } + } + } + + context("given some tagged input representing an object where the resulting type should be an interface (string)") { + val input = """ + test: ! + stringVal: "kudo" + """.trimIndent() + + context("parsing that input") { + val result = interfaceYaml.parse(InterfaceWrapper.serializer(), input) + it("deserializes it to a Kotlin object") { + assert(result).toBe(InterfaceWrapper(InterfaceString("kudo"))) + } + } + } + + context("given some tagged input representing a list of objects where the resulting type should be an interface") { + val input = """ + - test: ! + intVal: 321 + - test: ! + stringVal: "hello" + - test: ! + stringVal: "world" + - test: ! + intVal: 890 + """.trimIndent() + + context("parsing that input") { + val result = interfaceYaml.parse(InterfaceWrapper.serializer().list, input) + it("deserializes it to a Kotlin object") { + assert(result).toBe( + listOf( + InterfaceWrapper(InterfaceInt(321)), + InterfaceWrapper(InterfaceString("hello")), + InterfaceWrapper(InterfaceString("world")), + InterfaceWrapper(InterfaceInt(890)) + ) + ) + } + } + } + context("given some input representing an object with an unknown key") { val input = """ abc123: something @@ -857,7 +978,7 @@ object YamlReadingTest : Spek({ context("parsing that input") { it("throws an appropriate exception") { assert({ Yaml.default.parse(ComplexStructure.serializer(), input) }).toThrow { - message { toBe("Property name must not be a list, map or null value. (To use 'null' as a property name, enclose it in quotes.)") } + message { toBe("Property name must not be a list, map, null or tagged value. (To use 'null' as a property name, enclose it in quotes.)") } line { toBe(1) } column { toBe(1) } } diff --git a/src/test/kotlin/com/charleskorn/kaml/YamlSealedTest.kt b/src/test/kotlin/com/charleskorn/kaml/YamlSealedTest.kt new file mode 100644 index 00000000..6119a58a --- /dev/null +++ b/src/test/kotlin/com/charleskorn/kaml/YamlSealedTest.kt @@ -0,0 +1,95 @@ +/* + + Copyright 2018-2019 Charles Korn. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +package com.charleskorn.kaml + +import ch.tutteli.atrium.api.cc.en_GB.notToThrow +import ch.tutteli.atrium.api.cc.en_GB.toBe +import ch.tutteli.atrium.verbs.assert +import org.spekframework.spek2.Spek +import org.spekframework.spek2.style.specification.describe + +object YamlSealedTest : Spek({ + describe("a YAML sealed structure") { + describe("creating an instance") { + + context("creating a tagged node with a map") { + it("does not throw an exception") { + assert { + YamlTaggedNode( + "sealedInt", + YamlMap( + mapOf(YamlScalar("value", Location(1, 1)) to YamlScalar("5", Location(2, 1))), + Location(1, 1) + ) + ) + }.notToThrow() + } + } + } + + describe("testing equivalence") { + val tagged = YamlTaggedNode( + "tag", + YamlScalar("test", Location(4, 1)) + ) + + context("comparing it to the same instance") { + it("indicates that they are equivalent") { + assert(tagged.equivalentContentTo(tagged)).toBe(true) + } + } + + context("comparing it to another tagged node with a different tag") { + it("indicates that they are not equivalent") { + assert( + tagged.equivalentContentTo( + YamlTaggedNode( + "tag2", + YamlScalar("test", Location(4, 1)) + ) + ) + ).toBe(false) + } + } + + context("comparing it to another tagged node with different child node") { + it("indicates that they are not equivalent") { + assert( + tagged.equivalentContentTo( + YamlTaggedNode( + "tag", + YamlScalar("test2", Location(4, 1)) + ) + ) + ).toBe(false) + } + } + } + + describe("converting the content to a human-readable string") { + context("a tagged scalar") { + val map = YamlTaggedNode("tag", YamlScalar("test", Location(4, 1))) + + it("returns tag and child") { + assert(map.contentToString()).toBe("!tag 'test'") + } + } + } + } +}) diff --git a/src/test/kotlin/com/charleskorn/kaml/YamlWritingTest.kt b/src/test/kotlin/com/charleskorn/kaml/YamlWritingTest.kt index e391730c..e1400171 100644 --- a/src/test/kotlin/com/charleskorn/kaml/YamlWritingTest.kt +++ b/src/test/kotlin/com/charleskorn/kaml/YamlWritingTest.kt @@ -20,10 +20,17 @@ package com.charleskorn.kaml import ch.tutteli.atrium.api.cc.en_GB.toBe import ch.tutteli.atrium.verbs.assert +import com.charleskorn.kaml.testobjects.InterfaceInt +import com.charleskorn.kaml.testobjects.InterfaceString +import com.charleskorn.kaml.testobjects.InterfaceWrapper import com.charleskorn.kaml.testobjects.NestedObjects +import com.charleskorn.kaml.testobjects.SealedWrapper import com.charleskorn.kaml.testobjects.SimpleStructure import com.charleskorn.kaml.testobjects.Team import com.charleskorn.kaml.testobjects.TestEnum +import com.charleskorn.kaml.testobjects.TestSealedStructure +import com.charleskorn.kaml.testobjects.interfaceModule +import com.charleskorn.kaml.testobjects.sealedModule import kotlinx.serialization.Serializable import kotlinx.serialization.internal.BooleanSerializer import kotlinx.serialization.internal.ByteSerializer @@ -471,6 +478,118 @@ object YamlWritingTest : Spek({ } } + describe("handling sealed classes") { + val yaml = Yaml(context = sealedModule) + context("serializing int sealed class") { + val input = SealedWrapper(TestSealedStructure.SimpleSealedInt(5)) + val output = yaml.stringify(SealedWrapper.serializer(), input) + + it("returns the value serialized in the expected YAML form") { + assert(output).toBe( + """ + element: ! + value: 5 + """.trimIndent() + ) + } + } + + context("serializing string sealed class") { + val input = SealedWrapper(TestSealedStructure.SimpleSealedString("5")) + val output = yaml.stringify(SealedWrapper.serializer(), input) + + it("returns the value serialized in the expected YAML form") { + assert(output).toBe( + """ + element: ! + value: "5" + """.trimIndent() + ) + } + } + + context("serializing list of sealed class structures") { + val input = listOf( + TestSealedStructure.SimpleSealedInt(5), + TestSealedStructure.SimpleSealedString("some test"), + TestSealedStructure.SimpleSealedInt(-20), + TestSealedStructure.SimpleSealedString("another test") + ).map(::SealedWrapper) + val output = yaml.stringify(SealedWrapper.serializer().list, input) + + it("returns the value serialized in the expected YAML form") { + assert(output).toBe( + """ + - element: ! + value: 5 + - element: ! + value: "some test" + - element: ! + value: -20 + - element: ! + value: "another test" + """.trimIndent() + ) + } + } + } + + describe("handling interface") { + val yaml = Yaml(context = interfaceModule) + context("serializing int interface") { + val input = InterfaceWrapper(InterfaceInt(18)) + val output = yaml.stringify(InterfaceWrapper.serializer(), input) + + it("returns the value serialized in the expected YAML form") { + assert(output).toBe( + """ + test: ! + intVal: 18 + """.trimIndent() + ) + } + } + + context("serializing string interface class") { + val input = InterfaceWrapper(InterfaceString("great")) + val output = yaml.stringify(InterfaceWrapper.serializer(), input) + + it("returns the value serialized in the expected YAML form") { + assert(output).toBe( + """ + test: ! + stringVal: "great" + """.trimIndent() + ) + } + } + + context("serializing list of interface class structures") { + val input = listOf( + InterfaceInt(0), + InterfaceString("test some"), + InterfaceInt(42), + InterfaceString("qwerty") + ).map(::InterfaceWrapper) + val output = yaml.stringify(InterfaceWrapper.serializer().list, input) + + it("returns the value serialized in the expected YAML form") { + assert(output).toBe( + """ + - test: ! + intVal: 0 + - test: ! + stringVal: "test some" + - test: ! + intVal: 42 + - test: ! + stringVal: "qwerty" + """.trimIndent() + ) + } + } + } + describe("handling default values") { context("when encoding defaults") { val defaultEncoder = Yaml.default diff --git a/src/test/kotlin/com/charleskorn/kaml/testobjects/TestObjects.kt b/src/test/kotlin/com/charleskorn/kaml/testobjects/TestObjects.kt index 2e5efde7..205911f5 100644 --- a/src/test/kotlin/com/charleskorn/kaml/testobjects/TestObjects.kt +++ b/src/test/kotlin/com/charleskorn/kaml/testobjects/TestObjects.kt @@ -18,7 +18,10 @@ package com.charleskorn.kaml.testobjects +import kotlinx.serialization.Polymorphic +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlinx.serialization.modules.SerializersModule @Serializable data class SimpleStructure( @@ -40,3 +43,43 @@ enum class TestEnum { Value1, Value2 } + +sealed class TestSealedStructure { + @Serializable + @SerialName("sealedInt") + data class SimpleSealedInt(val value: Int) : TestSealedStructure() + + @Serializable + @SerialName("sealedString") + data class SimpleSealedString(val value: String) : TestSealedStructure() +} + +@Serializable +data class SealedWrapper(@Polymorphic val element: TestSealedStructure) + +interface TestInterface + +val sealedModule = SerializersModule { + polymorphic(TestSealedStructure::class) { + TestSealedStructure.SimpleSealedInt::class with TestSealedStructure.SimpleSealedInt.serializer() + TestSealedStructure.SimpleSealedString::class with TestSealedStructure.SimpleSealedString.serializer() + } +} + +@Serializable +@SerialName("interfaceInt") +data class InterfaceInt(val intVal: Int) : TestInterface + +@Serializable +@SerialName("interfaceString") +data class InterfaceString(val stringVal: String) : TestInterface + +@Serializable +data class InterfaceWrapper(val test: TestInterface) + +val interfaceModule = SerializersModule { + polymorphic(TestInterface::class) { + InterfaceInt::class with InterfaceInt.serializer() + InterfaceString::class with InterfaceString.serializer() + } +}