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

Protobuf oneOf #67

Closed
leksak opened this issue Jan 16, 2018 · 10 comments
Closed

Protobuf oneOf #67

leksak opened this issue Jan 16, 2018 · 10 comments

Comments

@leksak
Copy link

leksak commented Jan 16, 2018

Is support planned for protobufs oneOf? https://developers.google.com/protocol-buffers/docs/proto#using-oneof

@elizarov
Copy link
Contributor

elizarov commented Jan 23, 2018

Not in the near future, since they this Protobuf feature does not naturally map into Kotlin typesystem.

@sandwwraith
Copy link
Member

Perhaps they can be implemented by sealed classes, but that way requires a lot of hand-work and checking

@s-garg
Copy link

s-garg commented Jul 22, 2019

Is there a workaround for working with proto definitions which have oneOf?

@bezmax
Copy link

bezmax commented Oct 11, 2019

@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
)

@terrakok
Copy link

@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()
    }
}

😭 😭 😭

@edenman
Copy link
Contributor

edenman commented Jun 5, 2020

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:
edenman/kmpPlayground@c87d7a7#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.

@LouisCAD
Copy link
Contributor

LouisCAD commented Oct 17, 2022

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

@LouisCAD
Copy link
Contributor

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?

@LouisCAD
Copy link
Contributor

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?

@xiaozhikang0916
Copy link
Contributor

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

10 participants