Skip to content

Commit e5e703e

Browse files
docs for protobuf oneof
1 parent a69c227 commit e5e703e

13 files changed

+313
-217
lines changed

docs/formats.md

Lines changed: 70 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ stable, these are currently experimental features of Kotlin Serialization.
1818
* [Integer types](#integer-types)
1919
* [Lists as repeated fields](#lists-as-repeated-fields)
2020
* [Packed fields](#packed-fields)
21+
* [Oneof field (experimental)](#oneof-field-experimental)
2122
* [ProtoBuf schema generator (experimental)](#protobuf-schema-generator-experimental)
2223
* [Properties (experimental)](#properties-experimental)
2324
* [Custom formats (experimental)](#custom-formats-experimental)
@@ -435,6 +436,65 @@ Per the standard packed fields can only be used on primitive numeric types. The
435436
Per the [format description](https://developers.google.com/protocol-buffers/docs/encoding#packed) the parser ignores
436437
the annotation, but rather reads list in either packed or repeated format.
437438

439+
### Oneof field (experimental)
440+
441+
Kotlin Serialization `ProtoBuf` format supports [oneof](https://protobuf.dev/programming-guides/proto2/#oneof) fields
442+
base on the [Polymorphism](polymorphism.md).
443+
444+
You can declare a property of your class to be `oneof` by following the contracts:
445+
446+
* Declare an interface, or abstract class, in represent of the `oneof` group.
447+
* Declare the property with the type added above, annotated with `@ProtoOneOf(ids)`
448+
with all possible proto numbers, not `@ProtoNumber`.
449+
* Declare subclasses from the type with **only one property** each per the oneof group elements.
450+
* Annotated the subclasses with `@ProtoNumber` on the class declaration, not the property,
451+
per the oneof group elements and `@ProtoOneOf(ids)` above.
452+
453+
<!--- INCLUDE
454+
import kotlinx.serialization.*
455+
import kotlinx.serialization.protobuf.*
456+
-->
457+
458+
```kotlin
459+
@Serializable
460+
data class Data(
461+
@ProtoNumber(1) val name: String,
462+
@ProtoOneOf(2, 3) val phone: IPhoneType,
463+
)
464+
@Serializable sealed interface IPhoneType
465+
@Serializable @ProtoNumber(2) @JvmInline value class HomePhone(val number: String): IPhoneType
466+
@Serializable @ProtoNumber(3) data class WorkPhone(val number: String): IPhoneType
467+
468+
fun main() {
469+
val dataTom = Data("Tom", HomePhone("123"))
470+
val stringTom = ProtoBuf.encodeToHexString(dataTom)
471+
val dataJerry = Data("Jerry", WorkPhone("789"))
472+
val stringJerry = ProtoBuf.encodeToHexString(dataJerry)
473+
println(stringTom)
474+
println(stringJerry)
475+
println(ProtoBuf.decodeFromHexString<Data>(stringTom))
476+
println(ProtoBuf.decodeFromHexString<Data>(stringJerry))
477+
}
478+
```
479+
480+
> You can get the full code [here](../guide/example/example-formats-08.kt).
481+
482+
```text
483+
0a03546f6d1203313233
484+
0a054a657272791a03373839
485+
Data(name=Tom, phone=HomePhone(number=123))
486+
Data(name=Jerry, phone=WorkPhone(number=789))
487+
```
488+
489+
<!--- TEST -->
490+
491+
In [ProtoBuf diagnostic mode](https://protogen.marcgravell.com/decode) the first 2 lines on output is equivalent to
492+
493+
```
494+
Field #1: 0A String Length = 3, Hex = 03, UTF8 = "Tom" Field #2: 12 String Length = 3, Hex = 03, UTF8 = "123"
495+
Field #1: 0A String Length = 5, Hex = 05, UTF8 = "Jerry" Field #3: 1A String Length = 3, Hex = 03, UTF8 = "789"
496+
```
497+
438498
### ProtoBuf schema generator (experimental)
439499

440500
As mentioned above, when working with protocol buffers you usually use a ".proto" file and a code generator for your
@@ -467,15 +527,15 @@ fun main() {
467527
println(schemas)
468528
}
469529
```
470-
> You can get the full code [here](../guide/example/example-formats-08.kt).
530+
> You can get the full code [here](../guide/example/example-formats-09.kt).
471531
472532
Which would output as follows.
473533

474534
```text
475535
syntax = "proto2";
476536
477537
478-
// serial name 'example.exampleFormats08.SampleData'
538+
// serial name 'example.exampleFormats09.SampleData'
479539
message SampleData {
480540
required int64 amount = 1;
481541
optional string description = 2;
@@ -519,7 +579,7 @@ fun main() {
519579
}
520580
```
521581

522-
> You can get the full code [here](../guide/example/example-formats-09.kt).
582+
> You can get the full code [here](../guide/example/example-formats-10.kt).
523583
524584
The resulting map has dot-separated keys representing keys of the nested objects.
525585

@@ -599,7 +659,7 @@ fun main() {
599659
}
600660
```
601661

602-
> You can get the full code [here](../guide/example/example-formats-10.kt).
662+
> You can get the full code [here](../guide/example/example-formats-11.kt).
603663
604664
As a result, we got all the primitive values in our object graph visited and put into a list
605665
in _serial_ order.
@@ -701,7 +761,7 @@ fun main() {
701761
}
702762
```
703763

704-
> You can get the full code [here](../guide/example/example-formats-11.kt).
764+
> You can get the full code [here](../guide/example/example-formats-12.kt).
705765
706766
Now we can convert a list of primitives back to an object tree.
707767

@@ -792,7 +852,7 @@ fun main() {
792852
}
793853
-->
794854

795-
> You can get the full code [here](../guide/example/example-formats-12.kt).
855+
> You can get the full code [here](../guide/example/example-formats-13.kt).
796856
797857
<!--- TEST
798858
[kotlinx.serialization, kotlin, 9000]
@@ -899,7 +959,7 @@ fun main() {
899959
}
900960
```
901961

902-
> You can get the full code [here](../guide/example/example-formats-13.kt).
962+
> You can get the full code [here](../guide/example/example-formats-14.kt).
903963
904964
We see the size of the list added to the result, letting the decoder know where to stop.
905965

@@ -1011,7 +1071,7 @@ fun main() {
10111071

10121072
```
10131073

1014-
> You can get the full code [here](../guide/example/example-formats-14.kt).
1074+
> You can get the full code [here](../guide/example/example-formats-15.kt).
10151075
10161076
In the output we see how not-null`!!` and `NULL` marks are used.
10171077

@@ -1139,7 +1199,7 @@ fun main() {
11391199
}
11401200
```
11411201
1142-
> You can get the full code [here](../guide/example/example-formats-15.kt).
1202+
> You can get the full code [here](../guide/example/example-formats-16.kt).
11431203
11441204
As we can see, the result is a dense binary format that only contains the data that is being serialized.
11451205
It can be easily tweaked for any kind of domain-specific compact encoding.
@@ -1333,7 +1393,7 @@ fun main() {
13331393
}
13341394
```
13351395
1336-
> You can get the full code [here](../guide/example/example-formats-16.kt).
1396+
> You can get the full code [here](../guide/example/example-formats-17.kt).
13371397
13381398
As we can see, our custom byte array format is being used, with the compact encoding of its size in one byte.
13391399

docs/serialization-guide.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ Once the project is set up, we can start serializing some classes.
151151
* <a name='integer-types'></a>[Integer types](formats.md#integer-types)
152152
* <a name='lists-as-repeated-fields'></a>[Lists as repeated fields](formats.md#lists-as-repeated-fields)
153153
* <a name='packed-fields'></a>[Packed fields](formats.md#packed-fields)
154+
* <a name='oneof-field-experimental'></a>[Oneof field (experimental)](formats.md#oneof-field-experimental)
154155
* <a name='protobuf-schema-generator-experimental'></a>[ProtoBuf schema generator (experimental)](formats.md#protobuf-schema-generator-experimental)
155156
* <a name='properties-experimental'></a>[Properties (experimental)](formats.md#properties-experimental)
156157
* <a name='custom-formats-experimental'></a>[Custom formats (experimental)](formats.md#custom-formats-experimental)

guide/example/example-formats-08.kt

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,23 @@ package example.exampleFormats08
33

44
import kotlinx.serialization.*
55
import kotlinx.serialization.protobuf.*
6-
import kotlinx.serialization.protobuf.schema.ProtoBufSchemaGenerator
76

87
@Serializable
9-
data class SampleData(
10-
val amount: Long,
11-
val description: String?,
12-
val department: String = "QA"
8+
data class Data(
9+
@ProtoNumber(1) val name: String,
10+
@ProtoOneOf(2, 3) val phone: IPhoneType,
1311
)
12+
@Serializable sealed interface IPhoneType
13+
@Serializable @ProtoNumber(2) @JvmInline value class HomePhone(val number: String): IPhoneType
14+
@Serializable @ProtoNumber(3) data class WorkPhone(val number: String): IPhoneType
15+
1416
fun main() {
15-
val descriptors = listOf(SampleData.serializer().descriptor)
16-
val schemas = ProtoBufSchemaGenerator.generateSchemaText(descriptors)
17-
println(schemas)
17+
val dataTom = Data("Tom", HomePhone("123"))
18+
val stringTom = ProtoBuf.encodeToHexString(dataTom)
19+
val dataJerry = Data("Jerry", WorkPhone("789"))
20+
val stringJerry = ProtoBuf.encodeToHexString(dataJerry)
21+
println(stringTom)
22+
println(stringJerry)
23+
println(ProtoBuf.decodeFromHexString<Data>(stringTom))
24+
println(ProtoBuf.decodeFromHexString<Data>(stringJerry))
1825
}

guide/example/example-formats-09.kt

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,17 @@
22
package example.exampleFormats09
33

44
import kotlinx.serialization.*
5-
import kotlinx.serialization.properties.Properties // todo: remove when no longer needed
6-
import kotlinx.serialization.properties.*
5+
import kotlinx.serialization.protobuf.*
6+
import kotlinx.serialization.protobuf.schema.ProtoBufSchemaGenerator
77

88
@Serializable
9-
class Project(val name: String, val owner: User)
10-
11-
@Serializable
12-
class User(val name: String)
13-
9+
data class SampleData(
10+
val amount: Long,
11+
val description: String?,
12+
val department: String = "QA"
13+
)
1414
fun main() {
15-
val data = Project("kotlinx.serialization", User("kotlin"))
16-
val map = Properties.encodeToMap(data)
17-
map.forEach { (k, v) -> println("$k = $v") }
15+
val descriptors = listOf(SampleData.serializer().descriptor)
16+
val schemas = ProtoBufSchemaGenerator.generateSchemaText(descriptors)
17+
println(schemas)
1818
}

guide/example/example-formats-10.kt

Lines changed: 7 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,17 @@
22
package example.exampleFormats10
33

44
import kotlinx.serialization.*
5-
import kotlinx.serialization.descriptors.*
6-
import kotlinx.serialization.encoding.*
7-
import kotlinx.serialization.modules.*
8-
9-
class ListEncoder : AbstractEncoder() {
10-
val list = mutableListOf<Any>()
11-
12-
override val serializersModule: SerializersModule = EmptySerializersModule()
13-
14-
override fun encodeValue(value: Any) {
15-
list.add(value)
16-
}
17-
}
18-
19-
fun <T> encodeToList(serializer: SerializationStrategy<T>, value: T): List<Any> {
20-
val encoder = ListEncoder()
21-
encoder.encodeSerializableValue(serializer, value)
22-
return encoder.list
23-
}
24-
25-
inline fun <reified T> encodeToList(value: T) = encodeToList(serializer(), value)
5+
import kotlinx.serialization.properties.Properties // todo: remove when no longer needed
6+
import kotlinx.serialization.properties.*
267

278
@Serializable
28-
data class Project(val name: String, val owner: User, val votes: Int)
9+
class Project(val name: String, val owner: User)
2910

3011
@Serializable
31-
data class User(val name: String)
12+
class User(val name: String)
3213

3314
fun main() {
34-
val data = Project("kotlinx.serialization", User("kotlin"), 9000)
35-
println(encodeToList(data))
15+
val data = Project("kotlinx.serialization", User("kotlin"))
16+
val map = Properties.encodeToMap(data)
17+
map.forEach { (k, v) -> println("$k = $v") }
3618
}

guide/example/example-formats-11.kt

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -24,29 +24,6 @@ fun <T> encodeToList(serializer: SerializationStrategy<T>, value: T): List<Any>
2424

2525
inline fun <reified T> encodeToList(value: T) = encodeToList(serializer(), value)
2626

27-
class ListDecoder(val list: ArrayDeque<Any>) : AbstractDecoder() {
28-
private var elementIndex = 0
29-
30-
override val serializersModule: SerializersModule = EmptySerializersModule()
31-
32-
override fun decodeValue(): Any = list.removeFirst()
33-
34-
override fun decodeElementIndex(descriptor: SerialDescriptor): Int {
35-
if (elementIndex == descriptor.elementsCount) return CompositeDecoder.DECODE_DONE
36-
return elementIndex++
37-
}
38-
39-
override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder =
40-
ListDecoder(list)
41-
}
42-
43-
fun <T> decodeFromList(list: List<Any>, deserializer: DeserializationStrategy<T>): T {
44-
val decoder = ListDecoder(ArrayDeque(list))
45-
return decoder.decodeSerializableValue(deserializer)
46-
}
47-
48-
inline fun <reified T> decodeFromList(list: List<Any>): T = decodeFromList(list, serializer())
49-
5027
@Serializable
5128
data class Project(val name: String, val owner: User, val votes: Int)
5229

@@ -55,8 +32,5 @@ data class User(val name: String)
5532

5633
fun main() {
5734
val data = Project("kotlinx.serialization", User("kotlin"), 9000)
58-
val list = encodeToList(data)
59-
println(list)
60-
val obj = decodeFromList<Project>(list)
61-
println(obj)
35+
println(encodeToList(data))
6236
}

guide/example/example-formats-12.kt

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,8 @@ class ListDecoder(val list: ArrayDeque<Any>) : AbstractDecoder() {
3737
}
3838

3939
override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder =
40-
ListDecoder(list)
41-
42-
override fun decodeSequentially(): Boolean = true
43-
}
40+
ListDecoder(list)
41+
}
4442

4543
fun <T> decodeFromList(list: List<Any>, deserializer: DeserializationStrategy<T>): T {
4644
val decoder = ListDecoder(ArrayDeque(list))

guide/example/example-formats-13.kt

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,7 @@ class ListEncoder : AbstractEncoder() {
1313

1414
override fun encodeValue(value: Any) {
1515
list.add(value)
16-
}
17-
18-
override fun beginCollection(descriptor: SerialDescriptor, collectionSize: Int): CompositeEncoder {
19-
encodeInt(collectionSize)
20-
return this
21-
}
16+
}
2217
}
2318

2419
fun <T> encodeToList(serializer: SerializationStrategy<T>, value: T): List<Any> {
@@ -29,26 +24,23 @@ fun <T> encodeToList(serializer: SerializationStrategy<T>, value: T): List<Any>
2924

3025
inline fun <reified T> encodeToList(value: T) = encodeToList(serializer(), value)
3126

32-
class ListDecoder(val list: ArrayDeque<Any>, var elementsCount: Int = 0) : AbstractDecoder() {
27+
class ListDecoder(val list: ArrayDeque<Any>) : AbstractDecoder() {
3328
private var elementIndex = 0
3429

3530
override val serializersModule: SerializersModule = EmptySerializersModule()
3631

3732
override fun decodeValue(): Any = list.removeFirst()
38-
33+
3934
override fun decodeElementIndex(descriptor: SerialDescriptor): Int {
40-
if (elementIndex == elementsCount) return CompositeDecoder.DECODE_DONE
35+
if (elementIndex == descriptor.elementsCount) return CompositeDecoder.DECODE_DONE
4136
return elementIndex++
4237
}
4338

4439
override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder =
45-
ListDecoder(list, descriptor.elementsCount)
40+
ListDecoder(list)
4641

4742
override fun decodeSequentially(): Boolean = true
48-
49-
override fun decodeCollectionSize(descriptor: SerialDescriptor): Int =
50-
decodeInt().also { elementsCount = it }
51-
}
43+
}
5244

5345
fun <T> decodeFromList(list: List<Any>, deserializer: DeserializationStrategy<T>): T {
5446
val decoder = ListDecoder(ArrayDeque(list))
@@ -58,13 +50,13 @@ fun <T> decodeFromList(list: List<Any>, deserializer: DeserializationStrategy<T>
5850
inline fun <reified T> decodeFromList(list: List<Any>): T = decodeFromList(list, serializer())
5951

6052
@Serializable
61-
data class Project(val name: String, val owners: List<User>, val votes: Int)
53+
data class Project(val name: String, val owner: User, val votes: Int)
6254

6355
@Serializable
6456
data class User(val name: String)
6557

6658
fun main() {
67-
val data = Project("kotlinx.serialization", listOf(User("kotlin"), User("jetbrains")), 9000)
59+
val data = Project("kotlinx.serialization", User("kotlin"), 9000)
6860
val list = encodeToList(data)
6961
println(list)
7062
val obj = decodeFromList<Project>(list)

0 commit comments

Comments
 (0)