question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Custom Polymorphic Serialization

See original GitHub issue

Didn’t want to pollute the #29 feature request, so I’m opening a new question (or bug) issue. 😃

I tried to make a custom serializer as you suggested, following the official example https://github.com/Kotlin/kotlinx.serialization/blob/v0.20.0/runtime/commonTest/src/kotlinx/serialization/json/JsonTreeAndMapperTest.kt#L35 that is linked to from the docs about custom serializers.

In the GitHub action yml file you can have

outputs:
    foo:
        description: bar
        value: baz

runs:
    using: composite

or

outputs:
    foo:
        description: bar

runs:
    using: something else

Meaning the output has a mandatory value property if it is a composite action but no value property if it is a normal action.

Previously I didn’t support composite, so I had

@Serializable
data class GitHubAction(
        /* ... */
        val outputs: Map<String, Output>? = null,
        /* ... */
) {
    /* ... */
    @Serializable
    data class Output(
            val description: String
    )
    /* ... */
}

I now changed it to

@Serializable
data class GitHubAction(
        /* ... */
        val outputs: Map<String, Output>? = null,
        /* ... */
) {
    /* ... */
    sealed class Output {
        abstract val description: String

        data class NormalOutput(
                override val description: String
        ) : Output()

        data class CompositeOutput(
                override val description: String,
                val value: String
        ) : Output()

        @Serializer(forClass = Output::class)
        companion object : KSerializer<Output> {
            override val descriptor: SerialDescriptor = SerialDescriptor("net.kautler.dao.GitHubAction.Output", SEALED) {
                element("normal", SerialDescriptor("net.kautler.dao.GitHubAction.Output.NormalOutput") {
                    element("description", PrimitiveDescriptor("net.kautler.dao.GitHubAction.Output.NormalOutput.description", STRING))
                })
                element("composite", SerialDescriptor("net.kautler.dao.GitHubAction.Output.CompositeOutput") {
                    element("description", PrimitiveDescriptor("net.kautler.dao.GitHubAction.Output.CompositeOutput.description", STRING))
                    element("value", PrimitiveDescriptor("net.kautler.dao.GitHubAction.Output.CompositeOutput.value", STRING))
                })
            }

            override fun deserialize(decoder: Decoder): Output {
                TODO("deserialize: Not yet implemented")
            }

            override fun serialize(encoder: Encoder, value: Output) {
                TODO("serialize: Not yet implemented")
            }
        }
    }
    /* ... */
}

The problem is, that it seems to not being used. When I had a TODO() in the descriptor it failed accordingly. But when I now try to deserialize an action file, I get complaint from kaml about missing type tag instead of the custom serializer being used. Do I do something wrong or is there a bug in kaml?

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Comments:17 (9 by maintainers)

github_iconTop GitHub Comments

1reaction
charleskorncommented, Oct 3, 2021

The issue is that you need to call beginStructure twice: once to start decoding the contextual structure, and then again to decode the value itself. kaml is quite strict about this, while it seems that the built-in JSON format is far more lenient.

For example, the following appears to work in the sample project you provided:

 @OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class)
 object StringOrListSerializer : KSerializer<List<String>> {
   private val listSerializer: KSerializer<List<String>> = ListSerializer(String.serializer())
+  private val listDescriptor = listSerializer.descriptor
+  private val stringDescriptor = String.serializer().descriptor

   override val descriptor: SerialDescriptor = buildSerialDescriptor(
     StringOrListSerializer::class.java.name,
     SerialKind.CONTEXTUAL
   ) {
-    element("list", listSerialDescriptor<String>())
+    element("list", listDescriptor)
     element<String>("string")
   }
 
   override fun serialize(encoder: Encoder, value: List<String>) = listSerializer.serialize(encoder, value)

   override fun deserialize(decoder: Decoder): List<String> = decoder.fromJson() ?: decoder.fromYaml()

   private fun Decoder.fromJson(): List<String>? {
     val decoder = this as? JsonDecoder ?: return null
     return try {
       listOf(decoder.decodeString())
     } catch (e: Throwable) {
       decoder.decodeSerializableValue(listSerializer)
     }
   }

   private fun Decoder.fromYaml(): List<String> {
-    val decoder = (this as YamlInput).beginStructure(descriptor) as Decoder
-    return try {
-      listOf(decoder.decodeString())
-    } catch (e: IncorrectTypeException) {
-      decoder.decodeSerializableValue(listSerializer)
+    val decoder = this.beginStructure(descriptor) as YamlInput
+
+    return when (decoder.node) {
+      is YamlScalar -> decodeString(decoder)
+      is YamlList -> decodeList(decoder)
+      else -> throw IllegalArgumentException("Value is neither a list nor a string.")
     }
   }
-}
+
+  private fun decodeString(input: YamlInput): List<String> {
+    val decoder = input.beginStructure(stringDescriptor) as YamlInput
+
+    return listOf(decoder.decodeString()).also { decoder.endStructure(stringDescriptor) }
+  }
+
+  private fun decodeList(input: YamlInput): List<String> {
+    return input.decodeSerializableValue(listSerializer)
+  }
+}
1reaction
charleskorncommented, Aug 16, 2021

@QuinnBast, if the type field determines what Kotlin type to use, you don’t need to use a custom serializer, you can use built-in features of kotlinx.serialization and kaml.

https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md describes the polymorphism support available in kotlinx.serialization, and uses the built-in JSON format for its examples.

For kaml, you can configure the YAML parser to use a property for polymorphism by setting polymorphismStyle to PolymorphismStyle.Property, and setting polymorphismPropertyName to the name of the field you’d like to use to store type information. In your case, this is type, which is the default value of polymorphismPropertyName.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Polymorphic serialization for object types - .NET
NET 7, using the custom object converter defined in the Previous behavior section, the following code serializes as 42. That's because the ...
Read more >
Polymorphic Serialization and Deserialization - Code Maze
In this article, we will deal with the polymorphic serialization and ... custom converter infer the correct polymorphic type from the JSON ...
Read more >
How to master polymorphism and custom serializers in kotlinx ...
2. A custom @Serializer ... This one gives you full control over the serialization process. You have to adapt your models to add...
Read more >
Jackson's polymorphic serialization / deserialization AND ...
So the problem is how to access the type from the type id resolving at the custom deserializer??? java · json · jackson...
Read more >
Polymorphic deserialization with Jackson and no annotations
Simply, Jackson is a Java library to serialize and deserialize objects ... variable requests (polymorphism) in Jackson by using custom ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found