At StackHawk, we've been really happy using Kotlin with Protobuf for the last few years. We have been using the generated Java classes in our Kotlin code, which has worked out well. Recently Kotlin became a first-class language for the protoc compiler, and we are looking to switch to the Kotlin generated classes.
As part of this move, here are some tips and tricks we've learned:
Non-GRPC Protobuf Gradle Plugin Generating Kotlin Classes
The Protobuf Gradle Plugin now comes with built-in protoc compiler support for Kotlin. Unlike Java, the plugin does not enable Kotlin generation by default.
Here's an example Protobuf Gradle Plugin config to generate non-GRPC Kotlin:
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.19.4"
}
// Enable Kotlin generation
generateProtoTasks {
all().forEach {
it.builtins {
id("kotlin")
}
}
}
}
Kotlin extension functions for Protos
Adding extension functions to proto messages has proven very useful. Our protos are built and shared with other projects as a jar library. That way, we write extension functions in one place, and they're available to all projects.
Building a field with a function
One case we've had a few times is when we have several pieces of data used to create a single field on a proto. In the following example, the Alert message has a url that gets created with a specific format by two pieces of data, the host and port.
message Alert {
string url = 1;
}
fun handleAlert(host: String, url: String) {
val alert = alert {
url = "http://$host:$port/"
}
doSomethingInteresting(alert)
}
Rather than duplicate the code to add the prefix and colon in the middle of the url string, we can create an extension function to make sure the url variable is created consistently.
fun AlertKt.Dsl.url(host: String, path: String): AlertKt.Dsl {
url = "https://$host:$path"
return this
}
And then use it like so:
fun handleAlert(host: String, url: String) {
val alert = alert {
url(host, port)
}
doSomethingInteresting(alert)
}
Building a field with a custom builder class
Given the Alert proto definition above, we could also use a custom builder class to build the url field. This is useful when the proto definition can't change.
Here we define a function receiver and associated builder class:
fun url(block: UrlBuilder.() -> Unit): String = UrlBuilder().apply { block() }.build()
data class UrlBuilder(var host: String = "", var path: String = "") {
fun build(): String = "$host:$path"
}
With those, when we go to make an Alert, we can do:
fun handleAlert(host: String, url: String) {
val alert = alert {
url = url {
host = "app.test.company.com"
path = "/api/v2/user"
}
}
doSomethingInteresting(alert)
}
Functions for JSON Handling
We have some projects where we need to convert generated Protobuf objects to and from JSON.
An extension function to generate a JSON string of a given protobuf object works nicely.
fun Scan.toJson(): String = Gson().toJson(this)
For reading in, there are a few options. Here's an example of a simple function that takes the json string and returns a fully realized Scan.
fun fromJson(json: String): Scan = Gson(json, Scan::class.java)
Using the two functions is easy:
val json = scan.toJson()
val scanFromJson = fromJson(json)
Wrapping Proto subcollections
We have had a few instances where we've needed to independently export a proto message's collection as JSON. Here's an example to help illustrate:
message Alert {
string requestMethod = 1;
string host = 2;
string path = 3;
}
message Scan {
repeated Alert alerts = 1;
string id = 2;
int64 startedTimestamp = 3;
}
In this case, we need to generate a JSON array of the scan's alerts. We don't want to create a new proto message that just has a list of alerts. Instead, we can create a custom data class that just wraps the proto objects:
data class AlertsWrapper(val alerts: List<Alert>)
Then we can just add extension functions that know how to read and write the JSON strings:
fun Scan.alertsToJson(): String = Gson().toJson(AlertsWrapper(alertsList))
fun ScanKt.Dsl.alerts(json: String): ScanKt.Dsl {
alerts += Gson().fromJson(json, AlertsWrapper::class.java).alerts
return this
}
Then using them looks like this:
val alertsJson = scan.alertsToJson()
val newScan = scan {
alerts(alertsJson)
}
Example Code
All this code, including a working build.gradle.kts that uses the Protobuf Gradle plugin for non-GRPC use can be found at https://github.com/kaakaww/kotlin-extension-functions-for-protos