Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pass additional arguments into custom serializer #1384

Open
KevinBassaDevelopment opened this issue Mar 23, 2021 · 12 comments
Open

Pass additional arguments into custom serializer #1384

KevinBassaDevelopment opened this issue Mar 23, 2021 · 12 comments

Comments

@KevinBassaDevelopment
Copy link

KevinBassaDevelopment commented Mar 23, 2021

Is it possible to parse data that can have multiple types, and multiple names into 1 data model?

  1. Example JSON:
    { "doseNumberInt" = 5 }
    or
    { "doseNumberMyDose" = { myDose : { value = "5" } } }
    or
    { "doseNumberString" = { myDose = "5" }

  2. The model itself has just 1 property
    var doseNumber: Type?

How I would imaging a possible solution:

object MultiTypeSerializer : JsonContentPolymorphicSerializer<Type>(Type::class) {

    override fun selectDeserializer(content: JsonElement): DeserializationStrategy<out Type> {
        // How to: access variable name and additional properties here (e.g. multiple SerialNames)
        // How to: define multiple serial descriptors here?
        val actualType= todoJsonKey.replace(todoVariableName,"")  //e.g. "doseNumberMyDose".repalce("doseNumber","")
        return if (actualType.equals("String")) {
            StringTypeSerializer
        } else if (actualType.equals("MyDose")
            MyDoseSerializer
        else 
          throw NotImplementedException("type  ${actualType} not implemented in MultiTypeSerializer ")
    }
}

To achieve this, it would be required to pass additional information from the data model into the serializer (currently only SerialName?)

Is this a usecase that makes sense? Is there a way to achieve this with the current possibilities?

@shanshin
Copy link
Contributor

Could you describe the problem being solved in more detail?
Polymorphic serializers generally do not have a stable common structure, therefore the polymorphic serializer descriptor does not describe fields.
The structure and set of attributes are controlled only by you in the selectDeserializer function. You can only "spy" the attribute names through the descriptors of the target serializers e.g. MyDoseSerializer.descriptor. However, I recommend using global constants for serial names if they are needed in several places.

@shanshin
Copy link
Contributor

To show in more detail the capabilities of polymorphic deserialization, I will give the following example.
Model classes are

const val IntHolderAttributeName = "int32"
const val GeneratedOverriddenAttributeName = "overridden"

@Serializable(with = ParentSerializer::class)
open class Parent
data class IntHolder(val i: Int): Parent()
data class StringHolder(val text: String): Parent()
@Serializable
data class Generated(@SerialName(GeneratedOverriddenAttributeName) val a: Int, val b: String): Parent()

some of them have custom serializers

object IntHolderSerializer: KSerializer<IntHolder> {
    override val descriptor: SerialDescriptor = buildClassSerialDescriptor("IntHolder") {
        element<Int>(IntHolderAttributeName)
    }

    override fun serialize(encoder: Encoder, value: IntHolder) {
        encoder.encodeStructure(descriptor) {
            encodeIntElement(descriptor, 0, value.i)
        }
    }

    override fun deserialize(decoder: Decoder): IntHolder {
        return decoder.decodeStructure(descriptor) {
            val i = decodeIntElement(descriptor, 0)
            IntHolder(i)
        }
    }
}

object StringHolderSerializer: KSerializer<StringHolder> {
    override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("string holder", PrimitiveKind.STRING)

    override fun serialize(encoder: Encoder, value: StringHolder) {
        encoder.encodeString(value.text)
    }

    override fun deserialize(decoder: Decoder): StringHolder {
        return StringHolder(decoder.decodeString())
    }
}

and a polymorphic serializer is

object ParentSerializer : JsonContentPolymorphicSerializer<Parent>(Parent::class) {
    override fun selectDeserializer(element: JsonElement): DeserializationStrategy<out Parent> {
        return when {
            element is JsonPrimitive && element.isString -> StringHolderSerializer
            element is JsonObject && element.containsKey(IntHolderAttributeName) -> IntHolderSerializer
            element is JsonObject && element.containsKey(GeneratedOverriddenAttributeName) -> Generated.serializer()
            else -> throw IllegalArgumentException("Invalid JSON $element")
        }
    }
}

in this case, you can deserialize different JSON of different structure

    println(Json.decodeFromString<Parent>(""""some text""""))
    println(Json.decodeFromString<Parent>("""{"int32": 42}"""))
    println(Json.decodeFromString<Parent>("""{"overridden": 10, "b": "other text"}"""))

@KevinBassaDevelopment
Copy link
Author

Thank you for this example, I have a JsonContentPolymorphicSerializer like this in place for another usecase.

Here I have a different problem: The root-serialname can be different.

So applied to your example, the parent property itself could be named either "parentInt" or "parentString", and based on this I want to select the proper serializer.

So I would want to pass these 2 serial names into the JsonContentPolymorphicSerializer so I can choose the proper serializer based on them.

Currently I don't know how the same custom serializer can listen to multiple different keys (without having multiple properties).

@shanshin
Copy link
Contributor

@KevinBassaDevelopment something like that #203?

@KevinBassaDevelopment
Copy link
Author

This combined with the information in the serializer itself which is the 'current' key that has been triggered

@shanshin
Copy link
Contributor

In this case, your top-level serializer is actually polymorphic, since its structure is not fixed. Maybe it's better to implement it this way?

@KevinBassaDevelopment
Copy link
Author

There are a few properties like this used within many base classes.
So a solution on property level would result in 1 poly serializer with ~10 concrete serializers, where a solution on top level would result in 10 concrete serializer for each single top level class (~100 and growing)

#203 will already resolve the main part.

Is there any way to access the property name in

object MultiTypeSerializer : JsonContentPolymorphicSerializer<Type>(Type::class) {
     override fun selectDeserializer(content: JsonElement): DeserializationStrategy<out Type> 

similar to
override val descriptor: SerialDescriptor = dataSerializer.descriptor in KSerializer?

@shanshin
Copy link
Contributor

The suggestion is to change the signature of the function selectDeserializer to something like selectDeserializer(elementName: String?, content: JsonElement): DeserializationStrategy<out Type> ?

@KevinBassaDevelopment
Copy link
Author

Yes exactly. This would enable many possibilities for custom serializers, especially when combined with the new @JsonAlternativeNames(listOf("propName1", "propNameAlternative"))

KevinBassaDevelopment added a commit to KevinBassaDevelopment/kotlinx.serialization that referenced this issue Apr 28, 2021
@KevinBassaDevelopment
Copy link
Author

@shanshin I created a PR for this feature, maybe you could have a look at it.

@sandwwraith
Copy link
Member

Sorry for the delay — I saw your PR, and I'll return to it soon. Although I'm not sure these changes are really necessary for your use case. It is much easier to determine serializer by property name, yes, but without it, this task is still possible — you can check whether the content is JsonPrimitive or JsonObject, whether JsonObject contains keys and its types, etc, etc. Do you suggest adding elementName for convenience? Or is there any case for which it is absolutely necessary?

@KevinBassaDevelopment
Copy link
Author

KevinBassaDevelopment commented Jun 1, 2021

It is mandatory for having proper data validation.
If the property is "valueBoolean" = false it should be parsed as boolean. If it is "valueBoolean" = 123 it should throw an exception.
if its "valuePositiveNum" = 123, it should be parsed correctly.

In the datamodel its then just a abstract type 'value' , with whatever concrete type. In my use case there are many different options with many different validations, and knowing how the original property was named is absolutely important.

It could also be "textFamilyName" = "Last" or "textFirstName" = "First", in which's way I absolutly cannot tell with just the text value. Its the way the data is build up. The result should then be text="Last" // where text = class FirstNameString.

These are just some random examples, I hope they help to describe why I need to get this information out of the serializer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants