kotlinx.serialization icon indicating copy to clipboard operation
kotlinx.serialization copied to clipboard

Protobuf oneOf

Open leksak opened this issue 7 years ago • 10 comments

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

leksak avatar Jan 16 '18 15:01 leksak

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

elizarov avatar Jan 23 '18 08:01 elizarov

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

sandwwraith avatar Jan 24 '18 11:01 sandwwraith

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

s-garg avatar Jul 22 '19 14:07 s-garg

@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 avatar Oct 11 '19 21:10 bezmax

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

terrakok avatar Oct 17 '19 13:10 terrakok

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.

edenman avatar Jun 05 '20 03:06 edenman

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 avatar Oct 17 '22 21:10 LouisCAD

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 avatar Oct 17 '22 22:10 LouisCAD

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?

LouisCAD avatar Oct 17 '22 22:10 LouisCAD

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.

xiaozhikang0916 avatar Dec 26 '23 15:12 xiaozhikang0916