0

With a structure similar to the following:

@Serializable
sealed class Parameters

@Serializable
data class StringContainer(val value: String): Parameters()

@Serializable
data class IntContainer(val value: Int): Parameters()

@Serializable
data class MapContainer(val value: Map<String, Parameters>): Parameters()

// more such as list, bool and other fairly (in the context) straight forward types

And the following container class:

@Serializable
data class PluginConfiguration(
// other value
    val parameters: Parameters.MapContainer,
)

I want to reach a (de)serialization where the paramters are configured as a flexible json (or other) map, as one would usually expect:

{
    "parameters": {
        "key1": "String value",
        "key2": 12,
        "key3": {}
    }
}

And so on. Effectively creating a flexible structure that is still structured enough to not be completely uncontrolled as Any would be. There's a fairly clearly defined (de)serialization, but I cannot figure how to do this.

I've tried reading the following https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/serialization-guide.md

And I do have a hunch that a polymorphic serializer is needed, but so far I'm bumping in to either generic structures, which I believe is way overkill for my purpose or that it for some reason cannot find the serializer for my subclasses, when writing a custom serializer for Parameters.

Update

So using custom serializers combined with surrogate classes, most things are working. The current problem is when values are put into the map, I get a kotlin.IllegalStateException: Primitives cannot be serialized polymorphically with 'type' parameter. You can use 'JsonBuilder.useArrayPolymorphism' instead. Even when I enable array polymorphism this error arises

Rohde Fischer
  • 1,248
  • 2
  • 10
  • 32
  • That might fly, combined with: https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/serializers.md#composite-serializer-via-surrogate I will give it a try and come back – Rohde Fischer Jul 22 '22 at 13:53
  • Please ask a new question with the problem from your update :) – aSemy Jul 23 '22 at 07:41

1 Answers1

0

The answer with kotlinx deserialization: different types && scalar && arrays is basically the answer, and the one I will accept. However, for future use, the complete code to my solution is as follows:

Class hierarchy

@kotlinx.serialization.Serializable(with = ParametersSerializer::class)
sealed interface Parameters

@kotlinx.serialization.Serializable(with = IntContainerSerializer::class)
data class IntContainer(
    val value: Int
) : Parameters

@kotlinx.serialization.Serializable(with = StringContainerSerializer::class)
data class StringContainer(
    val value: String
) : Parameters

@kotlinx.serialization.Serializable(with = MapContainerSerializer::class)
data class MapContainer(
    val value: Map<String, Parameters>
) : Parameters

@kotlinx.serialization.Serializable
data class PluginConfiguration(
    val plugin: String,
    val parameters: MenuRunnerTest.MapContainer
)

Serializers:

abstract class BaseParametersSerializer<T : Parameters> : KSerializer<T> {
    override val descriptor: SerialDescriptor = JsonElement.serializer().descriptor

    override fun serialize(encoder: Encoder, value: T) {
        fun toJsonElement(value: Parameters): JsonElement = when (value) {
            is IntContainer -> JsonPrimitive(value.value)
            is MapContainer -> JsonObject(
                value.value.mapValues { toJsonElement(it.value) }
            )
            is StringContainer -> JsonPrimitive(value.value)
        }

        val sur = toJsonElement(value)

        encoder.encodeSerializableValue(JsonElement.serializer(), sur)
    }

    override fun deserialize(decoder: Decoder): T {
        with(decoder as JsonDecoder) {
            val jsonElement = decodeJsonElement()

            return deserializeJson(jsonElement)
        }
    }

    abstract fun deserializeJson(jsonElement: JsonElement): T
}

object ParametersSerializer : BaseParametersSerializer<Parameters>() {
    override fun deserializeJson(jsonElement: JsonElement): Parameters {
        return when(jsonElement) {
            is JsonPrimitive -> when {
                jsonElement.isString -> StringContainerSerializer.deserializeJson(jsonElement)
                else -> IntContainerSerializer.deserializeJson(jsonElement)
            }
            is JsonObject -> MapContainerSerializer.deserializeJson(jsonElement)
            else -> throw IllegalArgumentException("Only ints, strings and strings are allowed here")
        }
    }
}

object StringContainerSerializer : BaseParametersSerializer<StringContainer>() {
    override fun deserializeJson(jsonElement: JsonElement): StringContainer {
        return when(jsonElement) {
            is JsonPrimitive -> StringContainer(jsonElement.content)
            else -> throw IllegalArgumentException("Only strings are allowed here")
        }
    }
}

object IntContainerSerializer : BaseParametersSerializer<IntContainer>() {
    override fun deserializeJson(jsonElement: JsonElement): IntContainer {
        return when (jsonElement) {
            is JsonPrimitive -> IntContainer(jsonElement.int)
            else -> throw IllegalArgumentException("Only ints are allowed here")
        }
    }
}

object MapContainerSerializer : BaseParametersSerializer<MapContainer>() {
    override fun deserializeJson(jsonElement: JsonElement): MapContainer {
        return when (jsonElement) {
            is JsonObject -> MapContainer(jsonElement.mapValues { ParametersSerializer.deserializeJson(it.value) })
            else -> throw IllegalArgumentException("Only maps are allowed here")
        }
    }
}

This structure should be expandable for lists, doubles and other structures, not included in the example :)

Rohde Fischer
  • 1,248
  • 2
  • 10
  • 32