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

Polymorphic serializer, ignore unregistered objects when deserializing list of polymorphic objects #448

Closed
luca992 opened this issue May 1, 2019 · 12 comments
Assignees

Comments

@luca992
Copy link

luca992 commented May 1, 2019

I have made a serializer class for the json:api specificaiton.

@Serializable
data class JsonApiResponseItem(
        override val jsonapi: JsonApiVersion,
        override val included: List<@Polymorphic JsonApiObject>? = null,
        @Polymorphic val data: JsonApiObject): JsonApiResponse() {

}

included can return many different types of objects which I am successfully using the polymorphic feature to deserialize. But this only works if all possible object types are registered in the scope of class JsonApiObject for polymorphic serialization.

If any types are not registered an error like is thrown

io.ktor.client.call.ReceivePipelineException: Fail to run receive pipeline: 
kotlinx.serialization.SerializationException: example-type is not registered for polymorphic 
serialization in the scope of class api.jsonapi.JsonApiObject

Is it possible to ignore deserializing polymorphic type objects which are not registered for polymorphic serialization without throwing an error? This would really be helpful because it would prevent deserialization from throwing errors if any new objects added to the included list... and it would be convenient if don't actually need to deserialize some of the included objects

@luca992 luca992 added the feature label May 1, 2019
@sandwwraith
Copy link
Member

I think this feature is more about to skip deserialization of element in the list if an error occured. Because polymorphic serializer does not know whether it is in the list now or not, and it has to return something (e.g. if the same error occurs when deserializing the data field from your example, there is no meaningful way to recover). Also, this requires from each encoder the support of skipping malformed element, so the list deserializer could start deserializing next one.

@luca992
Copy link
Author

luca992 commented May 16, 2019

Yeah, that sounds right.
Maybe another option would be to allow this in nonstrict mode by allowing a backup serializer to be registered for polymorphic classes in SerializersModule.

maybe something along the lines of:

@Serializable
data class EmptyJsonApiObject : JsonApiObject()

Json(context = SerializersModule {
    polymorphic(JsonApiObject::class) {
        JsonApiObjectUser::class with JsonApiObjectUser.serializer()
        JsonApiObjectEmployee::class with JsonApiObjectEmployee.serializer()
        addBackUpSerializer(EmptyJsonApiObject.serializer())
    }
},
        configuration = JsonConfiguration(
                strictMode = false,
        )
)

//addBackUpSerializer would require EmptyJsonApiObject to extend JsonApiObject

So in nonStrict mode at least, instead of throwing kotlinx.serialization.SerializationException: example-type is not registered for polymorphic serialization in the scope of class api.jsonapi.JsonApiObject, it would attempt to de-serialize using EmptyJsonApiObject's deserializer. Which I think should work fine, because nonStrict allows deserializing to an empty class.

My suggestion sounds pretty hacky though. But might be something to consider until support for skipping malformed element can be added.

@outadoc
Copy link

outadoc commented Feb 5, 2020

I think this feature is more about to skip deserialization of element in the list if an error occured.

Hi— do you know if that's something that is possible at the moment? Or a planned feature? It sounds like a good solution for my use case.

@gregorbg
Copy link

gregorbg commented Feb 7, 2020

I'd like to +1 this issue / feature request, as I am running into a very similar situation.

In my particular use case, I am working with a common interchange format for a sports event hosted at the association's website.

There is a list of competitors and the association supplies basic data (name, nationality). There is an option to include extension fields, which contain any kind of data -- but they can be synced back to the central association API.

For example, one application may use this to store T-Shirt sizes. Another application may need to register whether the person is left- or right-handed etc. When they store that kind of information, it automatically becomes available in the general API for the competitors' list.

I want to be able to write and parse (hence the @Serializable feature) my own extensions about god knows what without having to parse T-Shirt sizes or left-handedness preferences from other applications.

@pdvrieze
Copy link
Contributor

pdvrieze commented Feb 7, 2020

@suushiemaniac I would say that the best option for your case is to create your own version of the Json format. The format is perfectly able to see that polymorphic serialization is needed and that no data type is needed. More importantly it may be able to use its own private way of determining the data type in the first place.

@gregorbg
Copy link

gregorbg commented Feb 7, 2020

@pdvrieze Thanks for the suggestion! Can you please specify what you mean by:

create your own version of the Json format

Unfortunately I don't have any control over the JSON specification used by the competition association website. The only API that I have access to yields a result like this (abridged):

{
    "eventName": "Super Serialize Competition",
    "competitors": [
        {
            "name": "John Doe",
            "nationality": "Nepal",
            "extensions": [
                {
                    "id": "polymorphicFoo",
                    "data": {
                        "yes": true,
                        "no": false
                    }
                },
                {
                    "id": "polymorphicBar",
                    "data": [
                        "one",
                        "two",
                        "three"
                    ]
                },
                {
                    "id": "polymorphicMyStuff",
                    "data": {
                        "favoriteMusic": "Jazz",
                        "likesLasagna": true
                    }
                }
            ]
        },
        {
            "name": "Jane Doe",
            "nationality": "Canada",
            "extensions": [...]
        }
    ]
}

Here, only polymorphicMyStuff was created and specified by me. The other two extension objects were created and uploaded by entirely different people but I see the output when querying the API as well.

I would really prefer if there was a skip unknown polymorphic entity feature (that could be enabled/disabled by config) as suggested above.

@pdvrieze
Copy link
Contributor

pdvrieze commented Feb 8, 2020

What I meant with format is basically an encoder/decoder pair. Looking at your data, you might however be able to get away with using an alternative (de)serializer instead, either for the list or the elements.

@gregorbg
Copy link

gregorbg commented Feb 8, 2020

Thanks for the explanation! I think I get your idea, but I believe it is a much larger effort than simply extending the library to allow for passing a skipUnknown flag somewhere.

Effectively right now I have to copy/paste your entire ListLikeSerializer source code (since it's all sealed within the library) except for one tiny modification as outlined above. This seems extremely overkill.

@pdvrieze
Copy link
Contributor

The trouble (also with SerialModule) for your case is that PolymorphicSerializer does not behave the way you'd like it to. There are two ways around that:

  • The format (encoder/decoder) remaps how polymorphic serialization actually works.
  • You use a different serializer than PolymorphicSerializer (either specified through @SerializeWith or by having a custom serializer for the parent - I would go with @SerializeWith)

This custom serializer for elements could be given as much knowledge about the data as desired. You could also, in the serializer special case the json (or any) format and handle it differently - for example by first parsing the data to JsonObject; detecting the actual type to use from that; and then deserializing the object with the (de)serializer that you determined you need. In case you want to ignore something you can then just return a default value of some sort (maybe encapsulate the json).

When understanding the serialization library, it is key to remember that fundamentally it is format independent. Because of that it cannot assume much about formats. For example, for some (binary) formats it is not possible to "skip" an unknown element as there is no size or end marker.

The format (json in this case) could however have a mode where missing keys (or even missing polymorphic values) are ignored - or fed to a "handler" that allows the user to specify the behaviour.

@gregorbg
Copy link

I definitely see your point here about the library being format-agnostic, @pdvrieze. For any other user who stumbles upon this issue, I also found the conversation in #514 to be very insightful.

However, it still seems like a desirable change to allow for custom (dynamic!) hooks being invoked via SerialModule context. Even with independent formats, the user would be given control as sort of a "last resort" before finally an exception is thrown. (see my linked #697 for details).

Ultimately, with the current state of the project it seems like the best option to write a custom serializer. While that does achieve my goal, I still insist that it feels wrong to (effectively) copy so much code with so few changes to get what I'm looking for. Thanks for the constructive discussion! :)

qwwdfsad added a commit that referenced this issue Mar 2, 2020
    * As explained in #697, it appears that concatenating two modules as "behaviours" (as opposed to "bag of serializers") is way too dangerous for the end users and implies a lot of inconsistent behaviours and performance hits. So the most rational solution here is to prohibit such concatenations by closing the way to implement your own SerialModule
    * In order to still solve "fallback serializer" use-case, new 'default' DSL is introduced along with its support in PolymorphicSerializer

Fixes #697
Fixes #448
qwwdfsad added a commit that referenced this issue Mar 2, 2020
    * As explained in #697, it appears that concatenating two modules as "behaviours" (as opposed to "bag of serializers") is way too dangerous for the end users and implies a lot of inconsistent behaviours and performance hits. So the most rational solution here is to prohibit such concatenations by closing the way to implement your own SerialModule
    * In order to still solve "fallback serializer" use-case, new 'default' DSL is introduced along with its support in PolymorphicSerializer

Fixes #697
Fixes #448
qwwdfsad added a commit that referenced this issue Mar 2, 2020
    * As explained in #697, it appears that concatenating two modules as "behaviours" (as opposed to "bag of serializers") is way too dangerous for the end users and implies a lot of inconsistent behaviours and performance hits. So the most rational solution here is to prohibit such concatenations by closing the way to implement your own SerialModule
    * In order to still solve "fallback serializer" use-case, new 'default' DSL is introduced along with its support in PolymorphicSerializer

Fixes #697
Fixes #448
qwwdfsad added a commit that referenced this issue Mar 18, 2020
    * As explained in #697, it appears that concatenating two modules as "behaviours" (as opposed to "bag of serializers") is way too dangerous for the end users and implies a lot of inconsistent behaviours and performance hits. So the most rational solution here is to prohibit such concatenations by closing the way to implement your own SerialModule
    * In order to still solve "fallback serializer" use-case, new 'default' DSL is introduced along with its support in PolymorphicSerializer

Fixes #697
Fixes #448
@werner77
Copy link

werner77 commented Aug 4, 2020

We solved this issue by adding a wrapper around polymorphic types:

/**
 * Serializable class which wraps a value which is optionally decoded (in case the client does not have support for
 * the corresponding value implementation).
 */
@Serializable(with = WrappedSerializer::class)
data class Wrapped<T : Any>(val value: T?) {
    fun unwrapped(default: T): T {
        return value ?: default
    }
}

using this serializer:

/**
 * Serializer for [Wrapped] values.
 */
class WrappedSerializer<T : Any>(private val valueSerializer: KSerializer<T?>) : KSerializer<Wrapped<T>> {
    override val descriptor: SerialDescriptor = valueSerializer.descriptor

    private val objectSerializer = JsonObject.serializer()

    override fun serialize(encoder: Encoder, value: Wrapped<T>) {
        valueSerializer.serialize(encoder, value.value)
    }

    override fun deserialize(decoder: Decoder): Wrapped<T> {
        val decoderProxy = DecoderProxy(decoder)
        return try {
            Wrapped(valueSerializer.deserialize(decoderProxy))
        } catch (ex: Exception) {
            // Consume the rest of the input if we are inside a structure
            decoderProxy.compositeDecoder?.let {
                decoderProxy.decodeSerializableValue(objectSerializer)
                it.endStructure(valueSerializer.descriptor)
            }
            Wrapped(null)
        }
    }
}

private class DecoderProxy(private val decoder: Decoder) : Decoder by decoder {

    var compositeDecoder: CompositeDecoder? = null

    override fun beginStructure(descriptor: SerialDescriptor, vararg typeParams: KSerializer<*>): CompositeDecoder {
        val compositeDecoder = decoder.beginStructure(descriptor, *typeParams)
        this.compositeDecoder = compositeDecoder
        return compositeDecoder
    }
}

and we defined wrapping/unwrapping extension functions for ease of use:

/**
 * Convenience function to unwrap a list of [Wrapped] values
 */
fun <T : Any> List<Wrapped<T>>.unwrapped(): List<T> {
    return this.mapNotNull { it.value }
}

/**
 * Convenience function to wrap a list of [Any] values
 */
fun <T : Any> List<T>.wrapped(): List<Wrapped<T>> {
    return this.map { Wrapped(it) }
}

This does work, all though of course it would be great if the library could perform this magic for us.

@ge-org
Copy link

ge-org commented Jan 9, 2022

I know this issue is pretty old by now, however, I just stumbled upon it since I had the same problem.
There's another solution available that doesn't require a wrapper or custom serializer.

We can register a default serializer for polymorphic types that will be used as a fallback. The downside compared to the wrapper solution is that we must register the serializer for every new polymorphic type we add. So it requires some discipline.

val json = Json {
    serializersModule += SerializersModule {
        polymorphic(MyPolymorphicType::class) {
            defaultDeserializer { MyPolymorphicType.Unknown.serializer() }
        }
    }
}

https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md#default-polymorphic-type-handler-for-deserialization

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

8 participants