Kotlin / kotlinx.serialization

Kotlin multiplatform / multi-format serialization
Apache License 2.0
5.39k stars 620 forks source link

Wrapping a type's serializer with another without changing the actual type #2699

Open Laxystem opened 4 months ago

Laxystem commented 4 months ago

What is your use-case and why do you need this feature?

I am implementing a JsonLD library for Kotlin/Multiplatform. However, I've encountered a problem: expanded JsonLD has lots of "useless" contracts like [{"@value":"etc..."}], meaning just "etc...".

To solve this issue, I create custom serializers that wrap the original type's serializer. There are three ways I can do so:

  1. Via an inline class wrapping the original type -- Functional<T> instead of T.
    • Problematic, I have to access the value via obj.property.value instead of just obj.property.
  2. Using @Serializable(with = Functional::class).
    • Much longer, isn't pretty, and it's easy to import the wrong Functional.
  3. Use typealiases. For example,

    typealias LdList = @Serializable(with = LdListSerializer::class) ImmutableList
    
    internal class LdListSerializer<E>(val impl: KSerializer<E>) : KSerializer<ImmutableList<E>> { /*...*/ }
    • This approach looks as if it would work perfectly, however, the following code results in an error.
      // Type alias expands to T, which is not a class, an interface, or an object.
      public typealias Functional<T> = @Serializable(with = FunctionalSerializier::class) T

Describe the solution you'd like

My initial solution

Add the with parameter to MetaSerializable, and allow serializers to get the serializer of the type they're of (serializer<T>() given @Functional T) as a constructor parameter.

And why it potentially won't work

The first part of my solution is alright. The second part, however, can be argued to be problematic design-wise - it could lead to annotations being chained order-dependently (something never seen before in Kotlin nor Java as far as I'm aware).

An alternative

Wait for Kotlin to support expanding type aliases to type parameters (see KT-68757).

pdvrieze commented 4 months ago
  1. Write a format that delegates to Json, but wraps serializers as required. (this is not as hard as it sounds, although some functions need to wrap their return types)
Laxystem commented 4 months ago

@pdvrieze although that makes sense for my use case (and I would love to know how to do that), it won't make sense for all use cases.

pdvrieze commented 4 months ago

@Laxystem Have a look at https://gist.github.com/pdvrieze/0eb34ab2f914383a4d98cfc414272ac7 This is a (simple) abstract class for StringFormat that has two abstract methods (determineEffectiveSerializer and determineEffectiveDeserializer). You could implement those to override the actual serializer used. The initial serializer/deserializer are "hacked" wrapped just to inject the actual format wrappers.

For some formats (such as XML) you might also need to handle wrapping the SerialDescriptors. For example XML build a document structure before actually (de)serializing. (Note that this particular class isn't needed for XML as XML has a policy that allows overriding (de)serializers already).

Laxystem commented 4 months ago

@pdvrieze thanks! Would you mind me using it as a reference for a MPL 2.0-licensed project?

pdvrieze commented 4 months ago

Go ahead

Laxystem commented 4 months ago
  1. Write a format that delegates to Json, but wraps serializers as required.

Problem: how can I identify when it is required? Intuitively, I think of annotations - but is there a better way? Afterall, afaik I can't use the .deocde/.encode functions to check, as I have to use them in order, and each only once.

pdvrieze commented 4 months ago

@Laxystem You can either use annotations with @SerialInfo as meta-annotation. Those will turn up in the descriptors.

Laxystem commented 4 months ago

Got it, thanks. I'll just add List, Set, and Map to the serializer module, so that I'll be able to check if a serializer is of them.

sandwwraith commented 2 weeks ago

Can't this be solved with just #292 ?