-
Notifications
You must be signed in to change notification settings - Fork 623
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
Support oneof declaraction in protobuf #2546
Support oneof declaraction in protobuf #2546
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like a good starting point. It probably makes sense to also allow non-sealed polymorphic children.
One thing I'm not sure that you handled is how to deal with child classes that are not annotated with @ProtoNum
(you may want to ignore them/throw an error on encoding).
formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/Helpers.kt
Outdated
Show resolved
Hide resolved
formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufDecoding.kt
Outdated
Show resolved
Hide resolved
formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufEncoding.kt
Outdated
Show resolved
Hide resolved
It should be an error in this case. Without the annotation it can never get a proper ProtoNumber since there is no guarantee on the order of the sub serializer, and not all But it is hard to find a proper time to throw an error. The best choise should be in compile time but a specific format has nothing to do with the plugin. Maybe perform a traversal check every time an oneof field is encountered? |
For a type that contains a The main thing to keep in mind is how it interacts with changed/updated classes (different library version/classloader shenanigans, etc.) when that is not direct under developer control (say you have a library that uses protobuf, some user implements one of the interfaces (when not sealed) and now the application doesn't work because the library uses protobuf internally... (maybe required annotations are something that the plugin could be made to support). Complicating is that deserialization can safely ignore types without |
I might argue that it would lead to inconsistency of encoding and decoding. The field annotated with ProtoOneOf has no number, and the proto number of property in child class may conflict with existing field and casue overwritting. The serializer cannot decode the same value from what it encoded to. This behaviour is error prone, hard to debug and confusing.
It could be some trouble theoretically, but it is more a core lib issue here in my opinion. The same usage may also fail when not using |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is possible to implement this without depending on AbstractPolymorphicSerializer. This implementation expects certain internal behaviour from the serialization library that is not part of its contract. Instead it can use the OneOfEncoder/Decoder to synthesize the expected structure for polymorphic types - and leave determining the (de)serializer to use to the actual serializer implementation (rather than assuming 2 cases and re-implementing their logic).
formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufDecoding.kt
Outdated
Show resolved
Hide resolved
formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufDecoding.kt
Outdated
Show resolved
Hide resolved
formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufEncoding.kt
Outdated
Show resolved
Hide resolved
formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufEncoding.kt
Show resolved
Hide resolved
formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufDecoding.kt
Outdated
Show resolved
Hide resolved
The shceme generator on current branch can only generate oneof scheme for sealed type, by the support of all subserializer encoded in the sealed serializer descriptor. For an open one, the What do you think of it? @rotilho |
Makes sense to me. I wonder though if using a repeatable @ProtobufOneOf to pair the num with a subclass wouldn't be a more straightforward approach. This, however, would force to know upfront all subclasses, probably not a problem for sealed but may be to open polymorphism. |
I would like to share my use case for context. Our groups are using proto3 syntex to define RPC methods and messages between clients(iOS and android) and servers(go). As we are adopting KMM in clients, I am migrating the data class in Java generated by proto plugin to kotlin data class, which are generated in compile time. In this scenario, all the data classes are static snd not editable, to match the definition of messages in proto files. So the oneof elements are enumerable, and Though supporting of open polymorphic has some meaning in theory, I still wonder if there are some actual use cases of it. In my opinion:
@rotilho As I said in the top description, this way of declaraction can do the work, but I don't think it is a good api, as developer need to write so many annotations on a property( in my case, there will be more than 20), making it unreadable. And for sealed interfaces which I prefer most, the mapping relations are settled at the moment they are defined, meaning that these mappings are redundent. |
e5e703e
to
533881d
Compare
Rewrote decoding in contract of common polymorphic serializer. But it still needs to find the actual serializer to get a proper serial name for polymorphic serializer. |
Sorry for my urging, but is there any work to do for this request to be accepted? |
Hey @pdvrieze. Any chance to take a look on this PR when you have some time? I'd love to be able to use it |
Hi @sandwwraith could you please also take a look on this PR whether it is qualified to be merged. I am looking forward to use it in my product code. |
@xiaozhikang0916 Sure thing, I'll take a look at it soon |
2ba6984
to
37b3c3e
Compare
Found some api check failed in the TeamCity check, but running Just remove it from commit history. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good job, I like the idea overall. However, I have two big design questions:
-
Is
val numbers
in@ProtoOneOf
really required? The annotation itself is necessary to differentiate between regular polymorphism and oneOf. However,numbers
in it seems excessive. First, we can already get them from descriptors using a mechanism similar todescriptor.getActualOneOfSerializer
. It is heavy processing, but result ofextractProtoOneOfIds
(the only place usingannotation.numbers
) is cached anyway. Second, it is too easy to make a mistake where you put@ProtoNumber
on a class but forget to update@ProtoOneOf
content. -
OneOf in protobuf is defined inside the outer message (i.e. class) itself, while we're using
@ProtoNumber
on a definition of a nested message. In other words, if I have@ProtoNumber(4) class X: SomeIface
, I have committed to use number 4 for it in everywhere. And if I already haveclass Y(@ProtoNumber(4) val s: String)
, I cannot addval x: SomeIface
to it, because I cannot override the number on a use-site ofoneof
class. This looks like a big design limitation; perhaps you can say more if it will bother people or not.
I haven't proof-read docs yet, but I've left comments about implementation. PTAL at the comments on tests — looks like they're working incorrectly with value classes.
formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufDecoding.kt
Outdated
Show resolved
Hide resolved
formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufDecoding.kt
Outdated
Show resolved
Hide resolved
formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufDecoding.kt
Outdated
Show resolved
Hide resolved
formats/protobuf/commonTest/src/kotlinx/serialization/protobuf/schema/SchemaValidationsTest.kt
Show resolved
Hide resolved
formats/protobuf/commonTest/src/kotlinx/serialization/protobuf/ProtobufOneOfTest.kt
Outdated
Show resolved
Hide resolved
Is val numbers in @ProtoOneOf really required?You are right, ProtoNumber of oneofIn a protobuf-file-based develop mode, the proto files will be the source-of-truth to generate data classes with some plugin. In the definition of protobuf, each In a kotlin-based develop mode, the case what you worry about may happen. But in practice, However, as a designed limitation, I would add some more explaination in the docs, and some runtime check in |
Perhaps. However, you can write Kotlin classes without a special proto plugin to be compatible with the existing schema. I'm not sure if it is popular in protobuf to reuse messages in |
Oh, and about
This is a bug in BCV (Kotlin/binary-compatibility-validator#141) which is fixed in 0.14.0. I'll update the version soon. However, there are some examples failing, probably due to a new duplicate check: https://teamcity.jetbrains.com/buildConfiguration/KotlinTools_KotlinxSerialization_RuntimeLibraryBuildAggregated/4559581?hideTestsFromDependencies=false&hideProblemsFromDependencies=false&expandBuildChangesSection=true&expandBuildTestsSection=true&expandBuildProblemsSection=true |
It is totally fine to reuse message between oneof fields. Let me elaborate a little bit. Giving messages like message ShareMessage {
string common = 1;
}
message Some {
string name = 1;
oneof pack {
ShareMessage share_in_some = 2;
}
} Definations of Now we share message Another {
string another_name = 1;
oneof another_pack {
ShareMessage share_in_another = 3;
}
} Note that we cannot share The oneof interface for previous class In summary, |
Thanks for the explanation. I've re-read everything — subclasses are meant to be used in one particular place, and there is even a requirement mentioned that they should have exactly one property. In that case, they don't have to be reusable. Is it possible though, to enforce 'subclass should have only one property' requirement via |
UPD: found |
Yes, both encoder and decoder have a check of elementsCount in |
formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/ProtoTypes.kt
Outdated
Show resolved
Hide resolved
formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/Helpers.kt
Outdated
Show resolved
Hide resolved
formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufEncoding.kt
Outdated
Show resolved
Hide resolved
formats/protobuf/commonTest/src/kotlinx/serialization/protobuf/ProtobufOneOfTest.kt
Outdated
Show resolved
Hide resolved
formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufDecoding.kt
Outdated
Show resolved
Hide resolved
formats/protobuf/commonTest/src/kotlinx/serialization/protobuf/ProtobufOneOfTest.kt
Outdated
Show resolved
Hide resolved
formats/protobuf/commonTest/src/kotlinx/serialization/protobuf/ProtobufOneOfTest.kt
Outdated
Show resolved
Hide resolved
formats/protobuf/commonTest/src/kotlinx/serialization/protobuf/ProtobufOneOfTest.kt
Outdated
Show resolved
Hide resolved
Yes, unfortunately we do not have any special mechanisms to provide formats with compile-time checks. |
I have re-thought about the design of oneof declaration. Is it necessary to annotate the message holder class with The message holder, as described above, should have only 1 property. We can still use the origin form of In this way, we need no to introduce another annotation target to In fact, there were some error found in my implementation and test case during this refactoring, which should be fixed right now. By the way, I think writing unit tests with data in binary format is quite cryptic here, e.g. hard to change the hex string after proto id updated. Maybe some utils like protoscope is necessary in preparing binary data. |
14de8d0
to
2ec2edc
Compare
2ec2edc
to
79dfb9d
Compare
It seems you need to run |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for such a big contribution, it was nice working with you!
Awesome guys! Thank you very much for working on this 🎉🎉🎉 |
It is an implementation of #2538 , to support
oneof
declaraction in protobuf.Some major changes:
ProtoOneOf
to mark a property of class should be decoded and encoded in the oneof way.ProtoNumber
now can be attached to a class, to mark the proto id if such class is an implementation of oneof field.SealedClassSerializer.subclassSerializers
now is a public property, because when decoding message with oneof mark, I need to find the related serializer of concrete class withProtoNumber
annotation, assisted by change 2.For change 3, there may be some other implementation to achieve it. For example, binding the proto id with specific serializer in
ProtoOneOf
annotation likeBut it may be tedious for user to write some super heavy tag for a property in data class, and hard to know that the protoNumber annotated in oneof field has priority to number in inner proerty. so current implementation became my choise.
If everything in this PR is OK, I will continue to update comments of code and docs.