Documenting gRPC services with OpenAPI v3


In this article, I’ll show how I generated an OpenAPI v3 specification from .proto
files for a gRPC service, using the protoc-gen-openapi
tool (from the Google/Gnostic project). I’ll also explain how to integrate this generation into a Kotlin/Gradle project in an automated way. The main motivation behind this was to improve visibility into gRPC services, making them available via web interfaces or importable into REST clients — simplifying HTTP call validation and integration. This is especially helpful because gRPC server reflection does not offer adequate visibility for teams consuming the service via HTTP.
1. Context and Motivation
While working on a project that exposed a gRPC service, I noticed that gRPC's native reflection wasn’t enough for teams consuming the service over HTTP. There wasn’t a centralized and reliable reference for standardizing calls — each team had its own collection, often outdated. This led to inconsistencies, hindered collaboration, and increased the risk of integration issues.
Although gRPC offers Server Reflection to expose available services and methods, this feature:
Is not enabled by default, requiring explicit configuration on the server. In my case, it wasn’t enabled.
Relies on external tools, like grpcurl or evans, to inspect the server.
For example, if reflection is enabled on the gRPC server, you can list available methods with:
grpcurl --plaintext localhost:50051 list
While this is useful for quick inspections, it doesn’t fulfill the need for OpenAPI-style REST documentation, which many teams rely on. That’s why I decided to automate the generation of OpenAPI documentation for the gRPC service, ensuring the documentation is always up-to-date, standardized, and accessible. This makes it easier to test endpoints and share knowledge across teams.
To make the process clear and reproducible, I created a reference repository that includes everything mentioned in this article: miguelsmuller/proto-to-openapi
2. Tools Evaluated
Before settling on protoc-gen-openapi
, I tested several options:
Tool | Type | OpenAPI | Maintainer | Notes |
protoc-gen-openapi | Binary (Go) | v3 | Google/Gnostic | Simple, maintained by the community. Gnostic |
protoc-gen-openapiv2 | Binary (Go) | v2 | grpc-ecosystem | Older, used by grpc-gateway . Accepts merge_file_name . grpc-gateway |
buf + google-gnostic-openapi | CLI + plugin | v3 | Buf + community | CI/CD-friendly. Requires buf.gen.yaml . buf.build |
namely/docker-protoc | Docker | v2 | Namely | Docker image with plugins pre-installed. docker-protoc |
3. Why I Chose protoc-gen-openapi
Some tools, like protoc-gen-openapiv2
, require a .desc
(descriptor set) file to generate documentation. This binary file contains service/message definitions and is generated with protoc
using the --descriptor_set_out
flag.
In my project, I already generated this .desc
because Envoy uses it for JSON ↔ gRPC transcoding.
However, protoc-gen-openapi
does not require a descriptor set. You can point it directly at the .proto
files, which makes configuration simpler. Plus:
It supports OpenAPI v3 out of the box.
It’s easy to configure, no need for descriptor sets or additional plugins.
It works as a standalone binary, written in Go and doesn't require
.jar
packaging.
This made protoc-gen-openapi
the lightest, most modern, and simplest option to integrate into the build process.
4. Why I Used Local Binaries
Even though protoc
is available as a .jar
for Gradle usage, the protoc-gen-openapi
plugin didn’t integrate well with that setup. So I opted to use local binaries for both protoc
and the plugin.
It’s worth noting that since the plugin is written in Go, your .proto
files must define a valid Go package path (with .
or /
), such as:
// DOES NOT WORK
option go_package = "customservice";
// WORKS
option go_package = "example/api/customservice/v1;customservice";
This is because the plugin expects a valid Go-style import path. Without it, generation may fail.
5. Adding Dependencies as Submodules
To ensure successful compilation, I added the following repositories as Git submodules:
The protocolbuffers/protobuf
repo is especially important since it contains the google.api.http
annotation discussed next. In my project, these dependencies were already present, but including them will help if you’re reproducing this setup from scratch.
6. Using google.api.http
in Your Protos
The protoc-gen-openapi
plugin needs the google.api.http
annotation to map each gRPC method to a REST-style HTTP route. In my case, these annotations were already present because Envoy also uses them for HTTP-to-gRPC conversion.
Example .proto
file:
syntax = "proto3";
package example.api.label.v1;
option go_package = "example/api/customservice/v1;customserviceZv1";
import "google/api/annotations.proto";
service CustomService {
rpc CreateLabel (MyRequest) returns (MyResponse) {
option (google.api.http) = {
post: "/v1/labels"
body: "*"
};
}
}
message MyRequest {
string id = 1;
}
message MyResponse {
string any_property = 1;
}
This annotation maps the CreateLabel
RPC to POST /v1/labels
in the OpenAPI file.
7. Manual Execution via Terminal
Before automating the process, I validated the generation manually:
./protoc \
--proto_path=protos \
--proto_path=third_party/template \
--proto_path=third_party/google-api-common-protos \
--proto_path=third_party/protocolbuffers-protobuf/src \
--plugin=protoc-gen-openapi=./protoc-gen-openapi-v3 \
--openapi_out=. \
third_party/template/annotations.proto \
third_party/template/template.proto \
$(find protos/ -name "*.proto")
--proto_path
tellsprotoc
where to look for.proto
files.--plugin
points to the OpenAPI plugin binary.--openapi_out
sets the output directory.
If everything is set up correctly, you’ll get a YAML OpenAPI file.
8. Automating with Gradle
To integrate OpenAPI generation into the build process (e.g., when running ./gradlew build
), I created a Gradle Exec
task. This ensures everyone on the team has access to updated documentation in the repo.
task generateOpenApi(type: Exec) {
group = 'build'
description = 'Generates OpenAPI spec from protos using protoc-gen-openapi'
def protoDir = "$projectDir/build/extracted-include-protos/main"
def outDir = "$projectDir/openapi"
def toolsDir = "$rootDir/gradle/tools"
doFirst {
file(outDir).mkdirs()
}
commandLine 'bash', '-c', """
\${toolsDir}/protoc \
--proto_path=\${protoDir} \
--proto_path=\${toolsDir}/template \
--proto_path=\${toolsDir}/google-api-common-protos \
--proto_path=\${toolsDir}/protocolbuffers-protobuf/src \
--plugin=protoc-gen-openapi=\${toolsDir}/protoc-gen-openapi-v3 \
--openapi_out=\${outDir} \
\${toolsDir}/template/annotations.proto \
\${toolsDir}/template/template.proto \
\$(find \${protoDir}/integration/api -name "*.proto")
"""
}
build.dependsOn generateOpenApi
9. Validating and Testing the OpenAPI
After generation, it’s a good idea to validate the openapi.yaml
file. Tools like Swagger Editor are great for catching syntax or structural issues.
You can also:
Import the spec into Postman or Insomnia to test routes.
Provide the spec to teams using Swagger UI or Redoc.
Eventually, I plan to publish this spec to our internal IDP (powered by backstage.io), making it more accessible and testable, even for non-technical teams. Here's a preview:
10. Common Issues and Fixes
Path issues: If
proto_path
is misconfigured,protoc
won’t find the files.Missing annotations: Without
google.api.http
, the plugin will only output messages — no REST endpoints.Version mismatches: Make sure
protoc
andprotoc-gen-openapi
versions are compatible.Go package misconfigurations:
.proto
files need a validoption go_package
or generation may fail.
If you encounter an issue not listed here, feel free to comment and we can investigate and update this section with new findings.
11. Conclusion and Next Steps
Using protoc-gen-openapi
from Gnostic solved my pain point of generating OpenAPI v3 documentation from .proto
files. It enabled me to:
Document gRPC contracts as REST APIs.
Simplify SDK creation and external team integration.
Automate documentation generation with Gradle, reducing human error.
If you're looking to bridge the gap between gRPC and REST, I hope this guide helps you save hours of trial and error. Feel free to leave a comment with questions or suggestions.
Happy integrating!
Subscribe to my newsletter
Read articles from Miguel Muller directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
