Protobuf oneOf
Is support planned for protobufs oneOf? https://developers.google.com/protocol-buffers/docs/proto#using-oneof
Not in the near future, since they this Protobuf feature does not naturally map into Kotlin typesystem.
Perhaps they can be implemented by sealed classes, but that way requires a lot of hand-work and checking
Is there a workaround for working with proto definitions which have oneOf?
@s-garg oneOf is a virtual type, it's not encoded in any special way in protobuf.
For example, given this proto:
message TestOneOf {
string id = 1;
oneof test_oneof {
string text = 4;
int32 number = 9;
}
}
You can deserialize it using:
@Serializable
data class TestOneOf(
@SerialId(1)
val id: String,
@SerialId(4)
val text: String? = null,
@SerialId(9)
val number: Int? = null
)
@bezmax you are right but for serialize i have to do this:
interface TestOneOf
@Serializable
data class TestText(
@SerialId(1) val id: String,
@SerialId(4) val text: String
): TestOneOf
@Serializable
data class TestInt(
@SerialId(1) val id: String,
@SerialId(9) val number: Int
): TestOneOf
//for serialization
val ctx = SerializersModule {
polymorphic<TestOneOf> {
TestText::class with TestText.serializer()
TestInt::class with TestInt.serializer()
}
}
:sob: :sob: :sob:
For anybody else who runs into this issue and wants to use sealed classes: if your oneof is the only field on the message, you can do something like this:
https://github.com/edenman/kmpPlayground/commit/c87d7a72586633a58b1b2da7173f607d8b3ddd35#diff-819a26e7b90a4189335ef52a05046708R18
(look up the list of sealed classes at runtime, decode to a json object and then delegate to the subclass's serializer. It's super ugly and I hate it, but such is life.
Here's an updated version for people that want protobuf polymorphic support.
In this test case, you can see kotlinx.serialization protobuf with a sealed interface, and it should also work with a sealed class:
import io.kotest.matchers.shouldBe
import io.kotest.matchers.types.shouldBeInstanceOf
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromByteArray
import kotlinx.serialization.encodeToByteArray
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic
import kotlinx.serialization.protobuf.ProtoBuf
import kotlinx.serialization.protobuf.ProtoNumber
import kotlin.test.Test
import kotlin.test.assertEquals
@Test
fun testSealedInterface() { // Put this function in a class in test sources.
val module = SerializersModule {
polymorphic(TestOneOf::class) {
subclass(TestText::class)
subclass(TestInt::class)
}
}
val protobuf = ProtoBuf { serializersModule = module }
TestText(id = "abc", "Kotlin!").let<TestOneOf, _> { initial ->
val byteArray = protobuf.encodeToByteArray(initial)
val deserialized = protobuf.decodeFromByteArray<TestOneOf>(byteArray)
deserialized.shouldBeInstanceOf<TestText>()
deserialized.text shouldBe "Kotlin!"
}
TestInt(id = "abc", 7).let<TestOneOf, _> { initial ->
val byteArray = protobuf.encodeToByteArray(initial)
val deserialized = protobuf.decodeFromByteArray<TestOneOf>(byteArray)
deserialized.shouldBeInstanceOf<TestInt>()
deserialized.number shouldBe 7
}
}
@Serializable
sealed interface TestOneOf
@Serializable
data class TestText(
@ProtoNumber(1) val id: String,
@ProtoNumber(4) val text: String
) : TestOneOf
@Serializable
data class TestInt(
@ProtoNumber(1) val id: String,
@ProtoNumber(9) val number: Int
) : TestOneOf
Wait, it also works if I use the unedited default Protobuf object 🤔
Has this now been resolved? Or is there some other trick that makes it work? Is there a way to diagnose the result without parsing binary with naked eye?
Looks like having the @Serializable annotation applied on the sealed interface does the trick.
Removing it and keep the custom serializerModule also makes the test pass.
I think the most important question is: is that using Protobuf's oneOf, or is that using something else like putting the name of the class in a hidden field?
I need to declare kotlin data class for proto structure by a custome code-gen plugin.
The workaround provided above is not suitable for my case because there may be more than 1 oneof field in the protocol message, and the generated classes will be exponential.
I come up with an idea, but need some help to support by the library.
Let's say I have a proto message like
message Person {
string name = 1;
oneof phone {
string mobile = 2;
string home = 3;
}
}
My favourite data class will be
data class Person(
val name: String,
val phone: IPhoneType,
)
sealed interface IPhoneType
data class MobilePhone(val value: String): IPhoneType
data class HomePhone(val value: String): IPhoneType
So I need to tell the ProtoBuf Decoder that if it comes with ProtoNum 2 or 3, deserialize it as IPhoneType and assign to the phone field.
A custom serializer for the whole Person class can work, but it would be nice to have some additional annotation supports. Like:
data class Person(
@ProtoNum(1) val name: String,
@ProtoOneOfFields(2, 3) val phone: IPhoneType,
)
sealed interface IPhoneType
@ProtoOneOfNum(2)
data class MobilePhone(val value: String): IPhoneType
@ProtoOneOfNum(3)
data class HomePhone(val value: String): IPhoneType
@ProtoOneOfFields tells that this field may be assined by the following ProtoNums, and the @ProtoOneOfNum on the concrete class tells which ProtoNum can be parsed to this type.