kotlinx.rpc 0.10.2 Help

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.

Building messages

Every generated message has a builder DSL accessed through the extension Companion.invoke operator:

import com.example.HelloRequest import com.example.invoke val request = HelloRequest { name = "kotlinx.rpc" }

Copying messages

Messages are immutable. Use copy extension to create a modified version:

import com.example.copy val updated = request.copy { name = "grpc" }

Enums

Proto enums become Kotlin sealed types. Unknown values received over the wire are represented as UNRECOGNIZED:

package com.example; enum Priority { PRIORITY_UNSPECIFIED = 0; HIGH = 1; LOW = 2; } message Task { required Priority priority = 1; }
import com.example.Priority import com.example.Task import com.example.invoke val task = Task { priority = Priority.HIGH }

Repeated fields

Repeated fields map to Kotlin List:

package com.example; message Numbers { repeated int32 values = 1; repeated string labels = 2; }
import com.example.Numbers import com.example.invoke import com.example.copy 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:

package com.example; message Config { map<string, int64> settings = 1; }
import com.example.Config import com.example.invoke import com.example.copy 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:

package com.example; message Event { oneof payload { string text = 1; int32 code = 2; } }
import com.example.Event import com.example.invoke 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:

package com.example; message Outer { message Inner { required int32 value = 1; } required Inner inner = 1; }
import com.example.Outer import com.example.invoke 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: 27 March 2026