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

Open polymorphism & Serializable interface #2517

Closed
hrach opened this issue Nov 30, 2023 · 4 comments
Closed

Open polymorphism & Serializable interface #2517

hrach opened this issue Nov 30, 2023 · 4 comments

Comments

@hrach
Copy link

hrach commented Nov 30, 2023

UseCase 1

There is an interface representing a destination. It can be implemented by a data class or object. I need to serialize/deserialize it. All my APIs need to consume these "destinations" instances:

interface Destination

fun navigate(destination: Destination) {
  val encode = MyEncoder()
  val deeplink = encode.serialize(destination)
}

This is the library part, devs create their own destination types implementing this interface.

Navigation to the destination is usually exposed via an event flow from ViewModel Flow<Destination>. It is not convenient to read and resolve those serializers by reifed inline generic - exposing Flow<Pair<Destinaion, KSerialize<...>> is too complex.

So the remaining option here is to use open polymorphism and build the SerializerModule with "auto" generated setup.

This is in short https://github.com/kiwicom/navigation-compose-typed library.


UseCase 2

We have an event-tracking (internal) library. There is something like

interface Event {
  val module: String
}

and in another Gradle module, there are different definitions of events:

data class ButtonClicked(
  val source: String,
) : Event {
  @Transient
  override val module = "profile"
}

Then we need to process those events and serialize them. So the API accepts the interface:

fun track(event: Event) {}

Because the event is passed through so many layers, it is also difficult to resolve its serializer via reifed inlined fun. So we are "misusing this":

@OptIn(InternalSerializationApi::class)
val eventSerializer = event::class.serializer() as KSerializer<Event>

Describe the solution you'd like

All those cases somehow do not need the "polymorhpishm serialization", we just need to serialize the particular passed instance.

The whole concept and problem seem to materialize to missing Serializable interface, which would simply promise that there will be a serializer for a particular class. This way the internals could be sure that the serializer is generated and it would consequently make the code compile-time type-safe.

Of course, the current API would stay, there would be just a third option to work with serializables. The interface would be an alternative to the annotation, similar to Swift's Codeable.

@hrach hrach added the feature label Nov 30, 2023
@pdvrieze
Copy link
Contributor

Normally you'd solve this with a data agnostic type (and custom serializer). For example with JsonElement for json. Unfortunately formats use the serialization descriptors of the input so it probably isn't possible to have a format agnostic version of this.

@sandwwraith
Copy link
Member

This was discussed once in #329 — you can read the comments there to understand why it's not feasible currently. In short, you need only to serialize classes in your use-case, but if you needed deserialization, you would quickly find out that such design is insufficient and makes deserialization impossible — you need the type in advance, i.e., full polymorphism with modules and "type" key in JSON. Also, event::class.serializer() won't work with generic classes (see the function documentation for the explanation why), and the proposed Serializable interface also won't (see linked issue for details).

@hrach
Copy link
Author

hrach commented Dec 1, 2023

@sandwwraith thanks for the response and link :)

you can read the comments there to understand why it's not feasible currently

TBH I still struggle.

you need only to serialize classes in your use-case, but if you needed deserialization, you would quickly find out that such design is insufficient and makes deserialization impossible

The first example is using deserialization as well. Let's forget generics for a moment. "you need the type in advance" - why it should be a limitation? In my example, We know the expected type so it doesn't limit us. And even if I wouldn't know it - why a standard polymorphism serializer couldn't be used? Json.decodeFromString<Destination>(json) seems pretty issueless with the "proposal". (The project has the type encoded, so it can be resolved).

Simply said, a Serializable interface is just a compile-time promise the serializer exists. The point of having this interface is for safe encoding, the decoding can stay as it was until now we don't miss it there - I do not understand what is the issue/complication here. I understand it is another way of marking class serializable.


From the referenced ticket:

 Classes with type parameters are one part of the problem. E.g. does List implement KSerializable? If so, what should its serializer function return?

I can see this gets difficult. Is it an issue that it won't implement KSerializable? IMHO not much. This could be the thing that type-classes could solve (later).

Another problem is Kotlin built-in types: primitives, collections, String etc. that obviously cannot implement an interface from the 3rd-party library.

I see this as a similar problem to coroutines. It could be a built-in interface. IMO not a blocker. Serialization seems to be as important as async programming.


It would be great if this could be documented in KEEP with enumerating limitations and these case evaluations, also some considerations and examples for Swift would be great as this is another kind of limitation (missing inline). Moreover, some comparison with Swift's Codeable would be a great read as well.

@pdvrieze
Copy link
Contributor

pdvrieze commented Dec 1, 2023

@hrach If you want to have a "general" serializable type. The way this could work is if you have a wrapper that holds both the serializer and the value. Then it would have a custom serializer that calls the stored serializer. However, this still does not work well with deserialization (you still need to find a way to determine what deserializer to use to create said wrapper/value).

As to SerialDescriptors, some formats may not ultimately require access in all cases, but others (e.g. XML is very flexible and uses the descriptors to know what particular things "mean").

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