Documenting gRPC services with OpenAPI v3

Miguel MullerMiguel Muller
6 min read

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:

ToolTypeOpenAPIMaintainerNotes
protoc-gen-openapiBinary (Go)v3Google/GnosticSimple, maintained by the community. Gnostic
protoc-gen-openapiv2Binary (Go)v2grpc-ecosystemOlder, used by grpc-gateway. Accepts merge_file_name. grpc-gateway
buf + google-gnostic-openapiCLI + pluginv3Buf + communityCI/CD-friendly. Requires buf.gen.yaml. buf.build
namely/docker-protocDockerv2NamelyDocker 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 tells protoc 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:

Prévia do OpenAPI no backstage.io


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 and protoc-gen-openapi versions are compatible.

  • Go package misconfigurations: .proto files need a valid option 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!

0
Subscribe to my newsletter

Read articles from Miguel Muller directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Miguel Muller
Miguel Muller