kotlinx.rpc 0.10.2 Help

Services

gRPC is serialization-agnostic by design, and kotlinx.rpc follows this principle. Protobuf is the most common choice, but you can use any serialization format by providing a custom GrpcMarshaller. kotlinx.serialization support is provided out of the box.

There are three ways to define a gRPC service, depending on how much code generation you need:

Full proto generation

This is the default path. You define both messages and the service in a .proto file, and the Gradle plugin generates all Kotlin code. You only need to implement the service interface.

Given the following schema:

syntax = "proto3"; message Image { bytes data = 1; } message RecogniseResult { int32 category = 1; } service ImageRecognizer { rpc recognize(Image) returns (RecogniseResult); }

The plugin generates the Image and RecogniseResult message types and the ImageRecognizer service interface annotated with @Grpc. You implement the service:

class ImageRecognizerImpl : ImageRecognizer { override suspend fun recognize(image: Image): RecogniseResult { val byte = image.data[0].toInt() return RecogniseResult { category = if (byte == 0) 0 else 1 } } }

Register it on the server and call it from the client:

val server = GrpcServer(8080) { registerService<ImageRecognizer> { ImageRecognizerImpl() } } server.start() server.awaitTermination()
val client = GrpcClient("localhost", 8080) { credentials = plaintext() } val recognizer = client.withService<ImageRecognizer>() val image = Image { data = byteArrayOf(0, 1, 2, 3) } val result = recognizer.recognize(image) println("Category: ${result.category}")

For details on what the protoc plugins and the compiler plugin generate, see Schema and codegen.

Partial proto generation

In this mode you only define messages in a .proto file (no service block) and write the @Grpc service interface yourself. This gives you full control over the service contract while still using generated Protobuf types.

Given the following schema:

syntax = "proto3"; message Image { bytes data = 1; } message RecogniseResult { int32 category = 1; }

Write the service interface by hand with the @Grpc annotation:

@Grpc interface ImageRecognizer { suspend fun recognize(image: Image): RecogniseResult }

The generated Image and RecogniseResult types already have built-in Protobuf marshallers, so no additional marshaller configuration is needed. The rest of the setup — implementation, server, and client — is identical to full proto generation.

No proto

You can skip .proto files entirely and write both the service interface and message types in Kotlin. In this case you must provide a GrpcMarshallerResolver so the framework knows how to serialize and deserialize your types over the wire.

Using kotlinx.serialization

The grpc-marshaller-kotlinx-serialization module provides a ready-made resolver that works with any kotlinx.serialization format. Add the dependency:

implementation("org.jetbrains.kotlinx:kotlinx-rpc-grpc-marshaller-kotlinx-serialization:<version>")

Define your types with @Serializable and your service with @Grpc:

@Serializable data class EchoMessage(val text: String) @Grpc interface EchoService { suspend fun echo(message: EchoMessage): EchoMessage } class EchoServiceImpl : EchoService { override suspend fun echo(message: EchoMessage): EchoMessage { return EchoMessage("${message.text} ${message.text}") } }

Pass the resolver when creating the server and client using asMarshallerResolver():

val server = GrpcServer(8080) { messageMarshallerResolver = Json.asMarshallerResolver() services { registerService<EchoService> { EchoServiceImpl() } } } server.start()
val client = GrpcClient("localhost", 8080) { messageMarshallerResolver = Json.asMarshallerResolver() credentials = plaintext() } val service = client.withService<EchoService>() val result = service.echo(EchoMessage("hello"))

Any StringFormat (such as Json) or BinaryFormat (such as Cbor or ProtoBuf) can be used.

Custom marshallers

For full control over serialization, implement GrpcMarshaller directly and provide a GrpcMarshallerResolver:

val stringMarshaller = object : GrpcMarshaller<String> { override fun encode(value: String, config: GrpcMarshallerConfig?): Source { return Buffer().apply { writeString(value) } } override fun decode(source: Source, config: GrpcMarshallerConfig?): String { return source.readString() } } val resolver = GrpcMarshallerResolver { kType -> when (kType.classifier) { String::class -> stringMarshaller else -> null } }

Pass the resolver to both server and client via messageMarshallerResolver, as shown above.

Alternatively, you can annotate individual message types with @WithGrpcMarshaller to bind a marshaller directly to a type. In this case no resolver is needed for that type:

@WithGrpcMarshaller(MyMessage.Marshaller::class) class MyMessage(val value: String) { object Marshaller : GrpcMarshaller<MyMessage> { override fun encode(value: MyMessage, config: GrpcMarshallerConfig?): Source { return Buffer().apply { writeString(value.value) } } override fun decode(source: Source, config: GrpcMarshallerConfig?): MyMessage { return MyMessage(source.readString()) } } }
Last modified: 10 March 2026