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

Fix serializing nulls for a property of a parameterized type with a nullable upper bound with Protobuf #2561

Conversation

ShreckYe
Copy link
Contributor

@ShreckYe ShreckYe commented Feb 3, 2024

No description provided.

… a nullable upper bound but break some existing tests

This is done by setting `nullableMode` depending on `serializer.descriptor.isNullable` in `encodeSerializableElement`.

Some common test functions are extracted to `TestFunctions.kt` to eliminate duplicate code.

This breaks some existing tests in `ProtobufCollectionsTest`.
…mit 16fc6de

A common `isMapOrList` function is extracted and the `elementKind` local variables is removed with this function so its value is evaluated only when needed.
@ShreckYe ShreckYe changed the title Support serializing nulls for a property of a parameterized type with a nullable upper bound Support serializing nulls for a property of a parameterized type with a nullable upper bound with Protobuf Feb 8, 2024
@ShreckYe
Copy link
Contributor Author

ShreckYe commented Mar 3, 2024

The checks on Linux failed because "Gradle build daemon disappeared unexpectedly (it may have been killed or may have crashed)". Is there a way to rerun it?

@ShreckYe
Copy link
Contributor Author

ShreckYe commented Mar 3, 2024

Here is some brief information for this PR.

See such a class definition with a type parameter T which has a nullable upper bound:

@Serializable
data class Box<T>(val value: T)

ProtoBuf.encodeToByteArray(Box(null)) fails with an exception:

kotlinx.serialization.SerializationException: 'null' is not allowed for not-null properties

While for JSON, Json.encodeToString(Box(null)) runs successfully with the result {"value":null}.

@pdvrieze
Copy link
Contributor

pdvrieze commented Mar 4, 2024

It looks like what you are trying to do is to omit the box if the member is empty. Is that not more of a case for either a custom serializer or a value class (which formats can handle specially). In terms of your tests, you at least need to test for correct behaviour if there are more members than just 1.
Beyond that, this behaviour would need to be configurable (and not default) as you are making an incompatible change to the format.

@ShreckYe
Copy link
Contributor Author

ShreckYe commented Mar 5, 2024

It looks like what you are trying to do is to omit the box if the member is empty.

No I don't think this is what I am doing. I mean it's probably a bug that, when the upper bound of a type of a property is nullable instead of the type itself, serializing a null fails for Protobuf. I will change the verb in the subject of this PR to "fix".

@ShreckYe ShreckYe changed the title Support serializing nulls for a property of a parameterized type with a nullable upper bound with Protobuf Fix serializing nulls for a property of a parameterized type with a nullable upper bound with Protobuf Mar 5, 2024
@pdvrieze
Copy link
Contributor

pdvrieze commented Mar 5, 2024

It looks like what you are trying to do is to omit the box if the member is empty.

No I don't think this is what I am doing. I mean it's probably a bug that, when the upper bound of a type of a property is nullable instead of the type itself, serializing a null fails for Protobuf. I will change the verb in the subject of this PR to "fix".

I was looking at the test data. The data is empty when the member is null, and not empty if there is a member. This looks like what happens for inline structs. Looking back it may be because you directly serialize the struct so the box is not tagged (as it is the outer type).

@ShreckYe
Copy link
Contributor Author

ShreckYe commented Mar 6, 2024

The data is empty when the member is null, and not empty if there is a member.

I think this is the correct behavior of Protobuf here. In the Protobuf Java library, writeTo() serializes a message just by serializing all its fields, and if all its optional fields are empty, the result is an empty message. There is a writeDelimitedTo() which writes the message size in varint encoding before the message (which is also how it serializes the data of a field of bytes, a nested message, or packed data), but it seems it's not supported here in this library yet.

If you run the code below on the dev branch the result is 0, which means the result is also an empty string.

class NullableIntBox(val value: Int?)
println(ProtoBuf.encodeToHexString(NullableIntBox(null)).length)

@cliffred
Copy link

cliffred commented Mar 6, 2024

I came across the same exception, and also another very similar one in org.mongodb:bson-kotlinx. But I hadn't seen this PR. I actually think the root cause is in the kotlinx.serialization compiler plugin. Because the default upper bound for a type parameter is Any?, they are nullable. Therefore I would expect that the write$Self$main static method generated by the kotlinx.serialization compiler plugin would invoke CompositeEncoder.encodeNullableSerializableElement, but instead it invokes CompositeEncoder.encodeSerializableElement. I've created JetBrains/kotlin#5271 to address this. This will also automatically fix this problem with the Protobuf encoder.

@ShreckYe
Copy link
Contributor Author

ShreckYe commented Mar 6, 2024

@cliffred Cool. I also played with the compiler plugin for a while and later found that this could be resolved without changing the code in the compiler plugin. I think both are viable solutions.

@cliffred
Copy link

cliffred commented Mar 6, 2024

Me too, it's indeed a good idea to also deal with this within the individual encoders, therefore I also submitted a PR to the MongoDB BSON kotlinx encoder to fix it also within that encoder.

Copy link
Member

@sandwwraith sandwwraith left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your PR! I believe this is the correct way to fix this in the encoder.

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

@Serializable
data class ExplicitNullableUpperNullablePropertyBoundBox<T : Any?>(val value: T?)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mind also adding test for class Foo<T: Any>(val t: T) to check that non-nullable type parameters still reject nulls?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sorry I am not sure how to do this. I could add some tests for class Foo<T: Any>(val t: T) but it's impossible to set a null for the member t, unless I go for some hacks like calling the constructor in Java and passing a null argument (I am not completely sure this works though).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, you're right, that's impossible for Kotlin's type system. Never mind :)

@sandwwraith
Copy link
Member

I've also taken a look at the PR by @cliffred. It seems to be the correct approach to my intuition, but I should re-check if it doesn't break anything.

@sandwwraith sandwwraith merged commit a2f92e4 into Kotlin:dev Mar 20, 2024
3 checks passed
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

Successfully merging this pull request may close these issues.

4 participants