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

Optional properties are not working on Kotlin 1.5.31 #1705

Closed
MikolajKakol opened this issue Sep 27, 2021 · 7 comments
Closed

Optional properties are not working on Kotlin 1.5.31 #1705

MikolajKakol opened this issue Sep 27, 2021 · 7 comments

Comments

@MikolajKakol
Copy link

MikolajKakol commented Sep 27, 2021

I'm dealing with demanding API, that has 3 states: value, null value, no value. It's a case described in blog post here: https://medium.com/livefront/kotlinx-serialization-de-serializing-jsons-nullable-optional-properties-442c7f0c2614

To Reproduce
I have serialiser as in blog post:

open class OptionalPropertySerializer<T>(
    private val valueSerializer: KSerializer<T>
) : KSerializer<OptionalProperty<T>> {
    final override val descriptor: SerialDescriptor = valueSerializer.descriptor

    final override fun deserialize(decoder: Decoder): OptionalProperty<T> =
        OptionalProperty.Present(valueSerializer.deserialize(decoder))

    final override fun serialize(encoder: Encoder, value: OptionalProperty<T>) {
        when (value) {
            OptionalProperty.NotPresent -> throw SerializationException(
                "Tried to serialize an optional property that had no value present." +
                        " Is encodeDefaults false?"
            )
            is OptionalProperty.Present ->
                valueSerializer.serialize(encoder, value.value)
        }
    }
}

on Kotlin 1.5.30 this valueSerializer was injected correctly. However on 1.5.31 I'm always getting there instance of: kotlinx.serialization.internal.UnitSerializer

Expected behavior
It should work as in Kotlin 1.5.30. I'm not sure what created that regression (https://github.com/JetBrains/kotlin/releases/tag/v1.5.31) or is it serialisation issue or Kotlin's.

Environment

  • Kotlin version: 1.5.31
  • Library version: 1.3.0, also tested with the same result on 1.2.2
  • Kotlin platforms: JVM, native
  • Gradle version: 7.2
@sandwwraith
Copy link
Member

Can you please show how you apply this serializer?

@MikolajKakol
Copy link
Author

MikolajKakol commented Sep 27, 2021

class OptionalPropertySerializer<T>(
    private val valueSerializer: KSerializer<T>
) : KSerializer<Optional<T>> {
    final override val descriptor: SerialDescriptor = valueSerializer.descriptor

    final override fun deserialize(decoder: Decoder): Optional<T> {
        return try {
            Optional.Value(valueSerializer.deserialize(decoder))
        } catch (exception: Exception) {
            L.e(exception)
            Optional.NotPresent
        }
    }

    final override fun serialize(encoder: Encoder, value: Optional<T>) {
        val msg = "Tried to serialize an optional property that had no value present. Is encodeDefaults false?"
        when (value) {
            Optional.NotPresent -> throw SerializationException(msg)
            is Optional.Value ->
                when (val optional = value.value) {
                    null -> encoder.encodeNull()
                    else -> valueSerializer.serialize(encoder, optional)
                }
        }
    }
}

@Serializable(OptionalPropertySerializer::class)
sealed class Optional<out T : Any?> {

    object NotPresent : Optional<Nothing>()

    data class Value<T : Any?>(val value: T?) : Optional<T>()

    fun get(): T? {
        return when (this) {
            NotPresent -> null
            is Value -> this.value
        }
    }
}

And then in data classes:

    @SerialName("credit_limit")
    val creditLimit: Optional<Money?> = Optional.NotPresent,
    @SerialName("insurance")
    val insurance: Optional<String?> = Optional.NotPresent,

I believe it's identical as in linked blog post.

Also I had breakpoint on line: Optional.Value(valueSerializer.deserialize(decoder)) and I saw that injected valueSerializer.deserialize was an serializer type that matched my type, like that "Money" or "String" serializer

@shanshin
Copy link
Contributor

As a workaround, you need to remove the @Serializable(OptionalPropertySerializer::class) annotation from the Optional class and add it to each property of your data class (like in blog post).

@DRSchlaubi
Copy link
Contributor

Still happens in 1.6.10 and 1.6.0 and is preventing us from upgrading

@DRSchlaubi
Copy link
Contributor

DRSchlaubi commented Dec 27, 2021

As a workaround, you need to remove the @Serializable(OptionalPropertySerializer::class) annotation from the Optional class and add it to each property of your data class (like in blog post).

That's not a feasible workaround, that class is used a couple of thousand times in our project

@DRSchlaubi
Copy link
Contributor

DRSchlaubi commented Dec 27, 2021

Tools -> Show Kotlin Bytecode I just checked the generated byte code from the compiler plugin, and it's completely identical

Edit 1:

If you look at the serializer for this class

@Serializable
data class Test(val optional: Optional<String?> = Optional.Missing())

It gives you this snippet

        @NotNull
        public KSerializer[] childSerializers() {
            return new KSerializer[]{new OptionalSerializer(BuiltinSerializersKt.getNullable((KSerializer)StringSerializer.INSTANCE))};
        }

But if you invoke (Test.serializer() as GeneratedSerializer).childSerializers() the dataSerializer becomes UnitSerializer

Edit 2:
If you run

BuiltinSerializersKt.getNullable((KSerializer)StringSerializer.INSTANCE)

Through the debugger you get a String? serializer

Detailed explanation why it's broken Edit 3: Now decompiling the output inside the buildfolder I found this lines
      @NotNull
      public KSerializer[] childSerializers() {
         KSerializer[] var1 = new KSerializer[]{Optional.Companion.serializer((KSerializer)UnitSerializer.INSTANCE), (KSerializer)IntSerializer.INSTANCE};
         return var1;
      }

// Snippets from deserialize method
            var6 = var8.decodeSerializableElement(var2, 0, (DeserializationStrategy)Optional.Companion.serializer((KSerializer)UnitSerializer.INSTANCE), var6);
            
             var6 = var8.decodeSerializableElement(var2, 0, (DeserializationStrategy)Optional.Companion.serializer((KSerializer)UnitSerializer.INSTANCE), var6);

(Full class: https://haste.schlaubi.me/xojosijopa.avrasm)

So it looks like the compiler plugin actually screws up here

Edit 4: It seems like this is the causing commit JetBrains/kotlin@3020e9c#diff-ca6fbd7989b6eb22df5d83397fd7bf6eb71ae4fbfb6dda8d260b14613a4f96fb
Edit 4.5
Specifically, this line here

val serializer = findStandardKotlinTypeSerializer(baseClass.module, context.irBuiltIns.unitType.toKotlinType())!!

Passing in context.irBuiltIns.unitType.toKotlinType() should be Unit, therefore findStandardKotlinTypeSerializer returns UnitSerializer

Edit 5:
I think I understand what is going on now

the line mentioned above has this comment

            // workaround for sealed and classes - the `serializer` **function expects non-null serializers, but does not use them, so serializers of any type can be passed**

Whilst this might be true about the auto generated serializers for sealed classes, it is definitivly not for the Optional serializer here, as it is custom and in fact makes use of the type used, therefore you can't just pass in Unit

My suggestion would be, to disable this kind of optimisation if the serializer is custom, although I don't know how to do that, as I am not that far into the compiler codebase

I figured out a new workaround, as the issue explained above only affects abstract and sealed classes, i just made my optional class open and made the constructor private to get the same "sealed" effect

This obviously misses out on the sealed advantages, like when statements recognizing all subclasses is calls as exhaustive, but it is a way easier workaround then what @shanshin suggested

@C2H6O
Copy link

C2H6O commented Jan 12, 2022

Related YouTrack ticket: https://youtrack.jetbrains.com/issue/KT-50764

shanshin added a commit to JetBrains/kotlin that referenced this issue Jan 25, 2022
shanshin added a commit that referenced this issue Jan 25, 2022
shanshin added a commit to JetBrains/kotlin that referenced this issue Jan 26, 2022
shanshin added a commit to JetBrains/kotlin that referenced this issue Jan 26, 2022
shanshin added a commit to JetBrains/kotlin that referenced this issue Jan 26, 2022
Alefas pushed a commit to JetBrains/kotlin that referenced this issue Jan 27, 2022
Alefas pushed a commit to JetBrains/kotlin that referenced this issue Jan 27, 2022
shanshin added a commit that referenced this issue Jul 12, 2022
sandwwraith pushed a commit that referenced this issue Sep 7, 2022
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

6 participants