# 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](https://github.com/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:

```bash
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](https://github.com/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](https://github.com/google/gnostic) |
| `protoc-gen-openapiv2` | Binary (Go) | v2 | grpc-ecosystem | Older, used by `grpc-gateway`. Accepts `merge_file_name`. [grpc-gateway](https://github.com/grpc-ecosystem/grpc-gateway) |
| `buf` + `google-gnostic-openapi` | CLI + plugin | v3 | Buf + community | CI/CD-friendly. Requires `buf.gen.yaml`. [buf.build](http://buf.build) |
| `namely/docker-protoc` | Docker | v2 | Namely | Docker image with plugins pre-installed. [docker-protoc](https://github.com/namely/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:

```plaintext
// 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:

* [`googleapis/api-common-protos.git`](mailto:git@github.com:googleapis/api-common-protos.git)
    
* [`protocolbuffers/protobuf.git`](mailto:git@github.com:protocolbuffers/protobuf.git)
    

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:

```plaintext
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:

```bash
./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.

```kotlin
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](https://editor.swagger.io/) 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**](http://backstage.io)), making it more accessible and testable, even for non-technical teams. Here's a preview:

![Prévia do OpenAPI no backstage.io](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/myxvr6097ccc35w200zt.png align="left")

---

## 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!
