Using generated code This page shows how to work with the Kotlin types generated from .proto schemas. For details on what the protoc plugins and the compiler plugin produce, see Schema and codegen .
If you use an IntelliJ-based IDE, install the Kotlin External FIR Support (KEFS) plugin so the editor can resolve compiler-generated declarations like Companion objects and Builder interfaces.
Building messages Every generated message has a builder DSL accessed through the Companion.invoke operator:
val request = HelloRequest {
name = "kotlinx.rpc"
}
Copying messages Messages are immutable. Use copy to create a modified version:
val updated = request.copy {
name = "grpc"
}
Enums Proto enums become Kotlin sealed types. Unknown values received over the wire are represented as UNRECOGNIZED:
enum Priority {
PRIORITY_UNSPECIFIED = 0;
HIGH = 1;
LOW = 2;
}
message Task {
Priority priority = 1;
}
val task = Task {
priority = Priority.HIGH
}
Repeated fields Repeated fields map to Kotlin List:
message Numbers {
repeated int32 values = 1;
repeated string labels = 2;
}
val numbers = Numbers {
values = listOf(1, 2, 3)
labels = listOf("a", "b", "c")
}
val extended = numbers.copy {
values = values + 4
}
Map fields Map fields map to Kotlin Map:
message Config {
map<string, int64> settings = 1;
}
val config = Config {
settings = mapOf("timeout" to 30L, "retries" to 3L)
}
val updated = config.copy {
settings = settings + ("retries" to 5L)
}
Oneof fields Oneof fields become Kotlin sealed interfaces. Each case is a data class wrapping the value:
message Event {
oneof payload {
string text = 1;
int32 code = 2;
}
}
val event = Event {
payload = Event.Payload.Text("hello")
}
when (event.payload) {
is Event.Payload.Text -> println(event.payload.value)
is Event.Payload.Code -> println(event.payload.value)
null -> {}
}
Nested messages Nested message types are accessed through the parent type:
message Outer {
message Inner {
int32 value = 1;
}
Inner inner = 1;
}
val outer = Outer {
inner = Outer.Inner {
value = 42
}
}
Streaming RPCs Streaming signatures use Flow. Given the following schema:
service Echo {
rpc ServerStream(EchoRequest) returns (stream EchoResponse);
rpc ClientStream(stream EchoRequest) returns (EchoResponse);
rpc BidiStream(stream EchoRequest) returns (stream EchoResponse);
}
The generated interface looks like this:
@Grpc
interface Echo {
fun ServerStream(message: EchoRequest): Flow<EchoResponse>
suspend fun ClientStream(message: Flow<EchoRequest>): EchoResponse
fun BidiStream(message: Flow<EchoRequest>): Flow<EchoResponse>
}
Implementation:
class EchoImpl : Echo {
override fun ServerStream(message: EchoRequest): Flow<EchoResponse> {
return flow {
repeat(3) { emit(EchoResponse { text = message.text }) }
}
}
override suspend fun ClientStream(message: Flow<EchoRequest>): EchoResponse {
return EchoResponse { text = message.last().text }
}
override fun BidiStream(message: Flow<EchoRequest>): Flow<EchoResponse> {
return message.map { EchoResponse { text = it.text } }
}
}
Client usage:
val echo = client.withService<Echo>()
// Server streaming
echo.ServerStream(EchoRequest { text = "hello" }).collect {
println(it.text)
}
// Client streaming
val result = echo.ClientStream(flow {
emit(EchoRequest { text = "one" })
emit(EchoRequest { text = "two" })
})
// Bidirectional streaming
echo.BidiStream(flow {
emit(EchoRequest { text = "ping" })
}).collect {
println(it.text)
}
Last modified: 10 March 2026