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

Implicit generic doesn't pick up typealias serializer #2264

Closed
BenWoodworth opened this issue Apr 7, 2023 · 15 comments
Closed

Implicit generic doesn't pick up typealias serializer #2264

BenWoodworth opened this issue Apr 7, 2023 · 15 comments

Comments

@BenWoodworth
Copy link
Contributor

BenWoodworth commented Apr 7, 2023

Describe the bug
When calling serializer() with an implicit type argument for a @Serializable typealias, the typealias is effectively ignored, and the actual type is used instead

To Reproduce

import kotlinx.serialization.*
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.*
import kotlinx.serialization.json.Json

private typealias SerializableMyClass = @Serializable(MyClassSerializer::class) MyClass
//                                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

class MyClass(val a: String)

object MyClassSerializer : KSerializer<MyClass> {
    override val descriptor: SerialDescriptor = String.serializer().descriptor

    override fun serialize(encoder: Encoder, value: MyClass): Unit =
        encoder.encodeString(value.a)

    override fun deserialize(decoder: Decoder): MyClass =
        MyClass(decoder.decodeString())
}

fun main(args: Array<String>) {
    val value = SerializableMyClass("A")

    Json.encodeToString(value) // Serializer for class 'MyClass' is not found.

    // inlines to this
    Json.run {
        encodeToString(serializersModule.serializer(), value) // Serializer for class 'MyClass' is not found.
    }

    // But providing a "redundant" explicit generic fixes the issue
    Json.run {
        encodeToString<SerializableMyClass>(serializersModule.serializer(), value)
        //            ^^^^^^^^^^^^^^^^^^^^^ IDE suggests "Remove explicit type arguments"
    }

    // Or here
    Json.run {
        encodeToString(serializersModule.serializer<SerializableMyClass>(), value)
        //                                         ^^^^^^^^^^^^^^^^^^^^^
    }

    // But because the argument is lost during inlining, providing the type here doesn't help
    Json.encodeToString<SerializableMyClass>(value) // Serializer for class 'MyClass' is not found.




    // And similarly, this does not work
    Json.encodeToString(serializer(), value) // Serializer for class 'MyClass' is not found.

    // But this does work
    Json.encodeToString(serializer<SerializableMyClass>(), value)
}

Expected behavior
Serializer provided in the typealias is used, even when the generic is implicit

Environment

  • Kotlin version: 1.8.20
  • Library version: 1.5.0
  • Kotlin platforms: JVM IR
  • Gradle version: 7.4.2
@osrl
Copy link

osrl commented Apr 11, 2023

I have a similar issue. So I didn't want to open a new issue, it might be related.

I’m trying to implement a serializer for a different kind of date format. which looks like this {"date": {"$date": 1688649908793} } This is a global type so I wanted to specify a serializer globally as documented here. Here is what I did:

@Serializable
data class Result(
    val date: EDate
)

typealias EDate = @Serializable(EDateSerializer::class) Instant

@Serializable
//@SerialName("EDate")
//@SerialName("Instant")
data class InstantSurrogate(
    @SerialName("\$date")
    val date: Long
)

//@OptIn(ExperimentalSerializationApi::class)
//@Serializer(forClass = Instant::class)
object EDateSerializer : KSerializer<Instant> {
    override val descriptor: SerialDescriptor = InstantSurrogate.serializer().descriptor

    override fun serialize(encoder: Encoder, value: Instant) {
        val millisecond = value.toEpochMilliseconds()
        val eDate = InstantSurrogate(millisecond)

        encoder.encodeSerializableValue(InstantSurrogate.serializer(), eDate)
    }

    override fun deserialize(decoder: Decoder): Instant {
        val eDate = decoder.decodeSerializableValue(InstantSurrogate.serializer())
        return Instant.fromEpochMilliseconds(eDate.date)
    }
}

When I do this, serializer tries to use InstantIso8601Serializer instead of mine. (I've also tried the commented out lines and implementing KSerializer<EDate>). Is there something I'm missing here? Is this related to the

@osrl
Copy link

osrl commented Apr 11, 2023

I guess this was related to #1895 . It works with 1.8.20. I'm not sure about @BenWoodworth 's issue

@sandwwraith
Copy link
Member

Unfortunately this is how type inference in Kotlin works (as it expands typealiases), so I'm not sure we can do anything here.

@BenWoodworth
Copy link
Contributor Author

BenWoodworth commented Apr 20, 2023

I was starting to think the same thing. I'll post an issue to the Kotlin youtrack and link it back here, since typealiases feels like something that should be propagated through inferred type args. And if it turns out to be something that won't be supported, then maybe leaving a note in the kxs docs about @Serializable in typealiases having that limitation

@sandwwraith
Copy link
Member

By the way, specifying value type explicitly in your example works around the issue: val value: SerializableMyClass = SerializableMyClass("A")

@iseki0
Copy link
Contributor

iseki0 commented Jul 10, 2023

@sandwwraith
Maybe #2358 is more problemetic. The official guide mentions this feature. But the feature is inconsistent when the original @Serialization annotation is exists.

@sandwwraith
Copy link
Member

As per https://youtrack.jetbrains.com/issue/KT-67252 and #2731, we won't support this use-case, so using explicit serializer is a way to go

@iseki0
Copy link
Contributor

iseki0 commented Jul 29, 2024

But in reality, it works in some cases. @sandwwraith

@sandwwraith
Copy link
Member

@iseki0 If you show us an example of such cases, we'll happily prohibit them as well 😄

@osrl
Copy link

osrl commented Aug 12, 2024

// {"date": {"$date": 1688649908793} } 
@Serializable
data class Result(
    val date: EDate
)

typealias EDate = @Serializable(EDateSerializer::class) Instant

@Serializable
@SerialName("EDate")
data class InstantSurrogate(
    @SerialName("\$date")
    val date: Long,
)

object EDateSerializer : KSerializer<Instant> {
    override val descriptor: SerialDescriptor = InstantSurrogate.serializer().descriptor

    override fun serialize(encoder: Encoder, value: Instant) {
        val millisecond = value.toEpochMilliseconds()
        val eDate = InstantSurrogate(millisecond)

        encoder.encodeSerializableValue(InstantSurrogate.serializer(), eDate)
    }

    override fun deserialize(decoder: Decoder): Instant {
        val eDate = decoder.decodeSerializableValue(InstantSurrogate.serializer())
        return Instant.fromEpochMilliseconds(eDate.date)
    }
}

this works on 1.7.1 and kotlin 2.0.0. do what you need to do 🔫

@iseki0
Copy link
Contributor

iseki0 commented Aug 12, 2024

Lol, @osrl I always call it JsonInstant,I don't agree that's a good idea, but I have no better solution.

@sandwwraith Could you add a compiler options for static serializer assignation?

@osrl
Copy link

osrl commented Aug 12, 2024

EDate is a spacial name for EJSON

I couldn't understand what's not a good idea @iseki0

@iseki0
Copy link
Contributor

iseki0 commented Aug 12, 2024

couldn't understand what's not a good idea @iseki0

You use a special name here. But for other tools? If there's also javax-serialization, databasex-serialization. How to do?

@osrl
Copy link

osrl commented Aug 12, 2024

Oh okay, as I said, it's a special name for a special json format. so it's okay.

@sandwwraith
Copy link
Member

@sandwwraith Could you add a compiler options for static serializer assignation?

you are probably talking about #507, but this is a feature that is postponed for a long time

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

No branches or pull requests

4 participants