A Kotlin DSL for programmatically generating Protocol Buffer 3 (proto3) schema files.
ProtoPoet provides a type-safe builder API that makes it easy to generate .proto files from Kotlin code — useful for code generators, schema registries, or any tooling that needs to produce protobuf definitions at runtime.
Gradle (Kotlin DSL):
dependencies {
implementation("dev.newspicel:protopoet:0.1.0")
}import dev.newspicel.protopoet.*
val proto = protoFile {
packageName = "example.v1"
fileComment("Generated by ProtoPoet")
import("google/protobuf/timestamp.proto")
fileOption("java_package", "com.example.v1")
message("Person") {
comment("Represents a person")
field(FieldType.STRING, "name", 1)
field(FieldType.INT32, "age", 2)
repeatedField(FieldType.STRING, "emails", 3)
optionalField(FieldType.STRING, "nickname", 4)
}
enum("Status") {
value("UNKNOWN", 0)
value("ACTIVE", 1)
value("INACTIVE", 2)
}
service("PersonService") {
rpc("GetPerson") {
request = "GetPersonRequest"
response = "Person"
}
}
}
println(proto)Output:
// Generated by ProtoPoet
syntax = "proto3";
package example.v1;
import "google/protobuf/timestamp.proto";
option java_package = "com.example.v1";
message Person {
// Represents a person
string name = 1;
int32 age = 2;
repeated string emails = 3;
optional string nickname = 4;
}
enum Status {
UNKNOWN = 0;
ACTIVE = 1;
INACTIVE = 2;
}
service PersonService {
rpc GetPerson (GetPersonRequest) returns (Person);
}Messages support all proto3 field types, nested messages, nested enums, field comments, reserved fields, and message-level options.
message("Person") {
comment("A person record")
field(FieldType.STRING, "name", 1)
repeatedField(FieldType.STRING, "tags", 2)
optionalField(FieldType.BOOL, "active", 3)
messageField("Address", "home_address", 4)
mapField(FieldType.STRING, FieldType.STRING, "metadata", 5)
oneof("contact") {
field(FieldType.STRING, "phone", 6)
field(FieldType.STRING, "email", 7)
}
message("Address") {
field(FieldType.STRING, "street", 1)
field(FieldType.STRING, "city", 2)
}
enum("PhoneType") {
value("MOBILE", 0)
value("HOME", 1)
}
reserved(10, 11, 12)
reservedNames("old_field")
reservedRange(100, 200)
}enum("Status") {
comment("Lifecycle status")
allowAlias()
value("UNKNOWN", 0)
value("ACTIVE", 1)
value("INACTIVE", 2)
reserved(5, 6)
reservedNames("DEPRECATED")
}Services support unary, client-streaming, server-streaming, and bidirectional-streaming RPCs with per-method options.
service("PersonService") {
comment("Manages person records")
rpc("GetPerson") {
request = "GetPersonRequest"
response = "Person"
}
rpc("ListPersons") {
request = "ListPersonsRequest"
response = "Person"
responseStreaming = true
}
}Define custom protobuf options via extend:
extension(OptionType.MESSAGE) {
field(FieldType.STRING, "my_option", 51234)
}
// Automatically imports google/protobuf/descriptor.protoprotoFile {
fileOption("java_package", "com.example.v1")
fileOption("java_multiple_files", true)
fileOption("go_package", "example.com/v1")
}In addition to the DSL, all specs expose a traditional builder API for cases where you construct definitions dynamically:
val msg = MessageSpec.builder("Person")
.addField(MessageFieldSpec.builder(FieldType.STRING, "name", 1).build())
.addField(MessageFieldSpec.builder(FieldType.INT32, "age", 2).build())
.build()
val proto = ProtoFile.builder()
.apply { packageName = "example.v1" }
.addMessage(msg)
.build()// To string
val source = proto.toString()
// To any Appendable (Writer, StringBuilder, etc.)
proto.writeTo(fileWriter)All proto3 scalar types are supported: DOUBLE, FLOAT, INT32, INT64, UINT32, UINT64, SINT32, SINT64, FIXED32, FIXED64, SFIXED32, SFIXED64, BOOL, STRING, BYTES, as well as MESSAGE and ENUM references.
ProtoPoet validates your definitions at build time:
- Duplicate field numbers within a message
- Duplicate field names within a message
- Duplicate type names (messages, enums) within a scope
- Conflicts between field numbers and reserved ranges/numbers
- Invalid field types in
oneofblocks (no maps, no repeated fields)
Licensed under the MIT License. See LICENSE for details.