You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
With `@ProtoOneOf` annotation for sealed classes and interfaces.
Inheritors of such an interface are expected to have one property with @ProtoNumber, encoded and decoded with special `OneOfEncoder/Decoder`. See documentation for design details.
Fixes#2538Fixes#67
@@ -435,6 +438,106 @@ Per the standard packed fields can only be used on primitive numeric types. The
435
438
Per the [format description](https://developers.google.com/protocol-buffers/docs/encoding#packed) the parser ignores
436
439
the annotation, but rather reads list in either packed or repeated format.
437
440
441
+
### Oneof field (experimental)
442
+
443
+
Kotlin Serialization `ProtoBuf` format supports [oneof](https://protobuf.dev/programming-guides/proto2/#oneof) fields
444
+
basing on the [Polymorphism](polymorphism.md) functionality.
445
+
446
+
#### Usage
447
+
448
+
Given a protobuf message defined like:
449
+
450
+
```proto
451
+
message Data {
452
+
required string name = 1;
453
+
oneof phone {
454
+
string home_phone = 2;
455
+
string work_phone = 3;
456
+
}
457
+
}
458
+
```
459
+
460
+
You can define a kotlin class semantically equal to this message by following these steps:
461
+
462
+
* Declare a sealed interface or abstract class, to represent of the `oneof` group, called *the oneof interface*. In our example, oneof interface is `IPhoneType`.
463
+
* Declare a Kotlin class as usual to represent the whole message (`class Data` in our example). In this class, add the property with oneof interface type, annotated with `@ProtoOneOf`. Do not use `@ProtoNumber` for that property.
464
+
* Declare subclasses for oneof interface, one per each oneof group element. Each class must have **exactly one property** with the corresponding oneof element type. In our example, these classes are `HomePhone` and `WorkPhone`.
465
+
* Annotate properties in subclasses with `@ProtoNumber`, according to original `oneof` definition. In our example, `val number: String` in `HomePhone` has `@ProtoNumber(2)` annotation, because of field `string home_phone = 2;` in `oneof phone`.
466
+
467
+
<!--- INCLUDE
468
+
import kotlinx.serialization.*
469
+
import kotlinx.serialization.protobuf.*
470
+
-->
471
+
472
+
```kotlin
473
+
// The outer class
474
+
@Serializable
475
+
data classData(
476
+
@ProtoNumber(1) valname:String,
477
+
@ProtoOneOf valphone:IPhoneType?,
478
+
)
479
+
480
+
// The oneof interface
481
+
@Serializable sealedinterfaceIPhoneType
482
+
483
+
// Message holder for home_phone
484
+
@Serializable @JvmInline value classHomePhone(@ProtoNumber(2) valnumber:String): IPhoneType
485
+
486
+
// Message holder for work_phone. Can also be a value class, but we leave it as `data` to demonstrate that both variants can be used.
487
+
@Serializable data classWorkPhone(@ProtoNumber(3) valnumber:String): IPhoneType
488
+
489
+
funmain() {
490
+
val dataTom =Data("Tom", HomePhone("123"))
491
+
val stringTom =ProtoBuf.encodeToHexString(dataTom)
492
+
val dataJerry =Data("Jerry", WorkPhone("789"))
493
+
val stringJerry =ProtoBuf.encodeToHexString(dataJerry)
You should note that each group of `oneof` types should be tied to exactly one data class, and it is better not to reuse it in
520
+
another data class. Otherwise, you may get id conflicts or `IllegalArgumentException` in runtime.
521
+
522
+
#### Alternative
523
+
524
+
You don't always need to apply the `@ProtoOneOf` form in your class for messages with `oneof` fields, if this class is only used for deserialization.
525
+
526
+
For example, the following class:
527
+
528
+
```
529
+
@Serializable
530
+
data class Data2(
531
+
@ProtoNumber(1) val name: String,
532
+
@ProtoNumber(2) val homeNumber: String? = null,
533
+
@ProtoNumber(3) val workNumber: String? = null,
534
+
)
535
+
```
536
+
537
+
is also compatible with the `message Data` given above, which means the same input can be deserialized into it instead of `Data` — in case you don't want to deal with sealed hierarchies.
538
+
539
+
But please note that there are no exclusivity checks. This means that if an instance of `Data2` has both (or none) `homeNumber` and `workNumber` as non-null values and is serialized to protobuf, it no longer complies with the original schema. If you send such data to another parser, one of the fields may be omitted, leading to an unknown issue.
540
+
438
541
### ProtoBuf schema generator (experimental)
439
542
440
543
As mentioned above, when working with protocol buffers you usually use a ".proto" file and a code generator for your
@@ -467,15 +570,15 @@ fun main() {
467
570
println(schemas)
468
571
}
469
572
```
470
-
> You can get the full code [here](../guide/example/example-formats-08.kt).
573
+
> You can get the full code [here](../guide/example/example-formats-09.kt).
471
574
472
575
Which would output as follows.
473
576
474
577
```text
475
578
syntax = "proto2";
476
579
477
580
478
-
// serial name 'example.exampleFormats08.SampleData'
581
+
// serial name 'example.exampleFormats09.SampleData'
479
582
message SampleData {
480
583
required int64 amount = 1;
481
584
optional string description = 2;
@@ -519,7 +622,7 @@ fun main() {
519
622
}
520
623
```
521
624
522
-
> You can get the full code [here](../guide/example/example-formats-09.kt).
625
+
> You can get the full code [here](../guide/example/example-formats-10.kt).
523
626
524
627
The resulting map has dot-separated keys representing keys of the nested objects.
525
628
@@ -599,7 +702,7 @@ fun main() {
599
702
}
600
703
```
601
704
602
-
> You can get the full code [here](../guide/example/example-formats-10.kt).
705
+
> You can get the full code [here](../guide/example/example-formats-11.kt).
603
706
604
707
As a result, we got all the primitive values in our object graph visited and put into a list
605
708
in _serial_ order.
@@ -701,7 +804,7 @@ fun main() {
701
804
}
702
805
```
703
806
704
-
> You can get the full code [here](../guide/example/example-formats-11.kt).
807
+
> You can get the full code [here](../guide/example/example-formats-12.kt).
705
808
706
809
Now we can convert a list of primitives back to an object tree.
707
810
@@ -792,7 +895,7 @@ fun main() {
792
895
}
793
896
-->
794
897
795
-
> You can get the full code [here](../guide/example/example-formats-12.kt).
898
+
> You can get the full code [here](../guide/example/example-formats-13.kt).
796
899
797
900
<!--- TEST
798
901
[kotlinx.serialization, kotlin, 9000]
@@ -899,7 +1002,7 @@ fun main() {
899
1002
}
900
1003
```
901
1004
902
-
> You can get the full code [here](../guide/example/example-formats-13.kt).
1005
+
> You can get the full code [here](../guide/example/example-formats-14.kt).
903
1006
904
1007
We see the size of the list added to the result, letting the decoder know where to stop.
905
1008
@@ -1011,7 +1114,7 @@ fun main() {
1011
1114
1012
1115
```
1013
1116
1014
-
> You can get the full code [here](../guide/example/example-formats-14.kt).
1117
+
> You can get the full code [here](../guide/example/example-formats-15.kt).
1015
1118
1016
1119
In the output we see how not-null`!!` and `NULL` marks are used.
1017
1120
@@ -1139,7 +1242,7 @@ fun main() {
1139
1242
}
1140
1243
```
1141
1244
1142
-
> You can get the full code [here](../guide/example/example-formats-15.kt).
1245
+
> You can get the full code [here](../guide/example/example-formats-16.kt).
1143
1246
1144
1247
As we can see, the result is a dense binary format that only contains the data that is being serialized.
1145
1248
It can be easily tweaked for any kind of domain-specific compact encoding.
@@ -1333,7 +1436,7 @@ fun main() {
1333
1436
}
1334
1437
```
1335
1438
1336
-
> You can get the full code [here](../guide/example/example-formats-16.kt).
1439
+
> You can get the full code [here](../guide/example/example-formats-17.kt).
1337
1440
1338
1441
As we can see, our custom byte array format is being used, with the compact encoding of its size in one byte.
else->throwIllegalArgumentException("Class ${this.serialName} should be abstract or sealed or interface to be used as @ProtoOneOf property.")
120
+
}.onEach { desc ->
121
+
if (desc.getElementAnnotations(0).none { anno -> anno isProtoNumber }) {
122
+
throwIllegalArgumentException("${desc.serialName} implementing oneOf type ${this.serialName} should have @ProtoNumber annotation in its single property.")
0 commit comments