Sansan Tech Blog

Sansanのものづくりを支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信

protoc-gen-connect-openapiを使ってConnect RPCのAPIドキュメントをいい感じに生成する方法

この記事はSansan Advent Calendar 2025 - Adventarの2日目の記事です🎄

はじめに

こんにちは!技術本部Platform Engineering Unitの樋口です。
これまでBill One EUの一員として認証基盤を開発していましたが、2025年6月に新設されたPlatform EUへチームごと異動し引き続き認証基盤を担当しています。

さて、認証基盤では基盤を利用するプロダクトのユーザー管理などのためにAPIを用意しており、APIのプロトコルとしてConnect RPCを採用しています。
今回は、Connect RPCのスキーマ情報であるProtocol Buffers(以下Protobuf)から、いい感じにAPIドキュメントを生成する方法をご紹介します。

背景

認証基盤は、OpenID Connectを利用してプロダクトと連携しますが、ユーザー管理などのためにConnect RPCでAPIも提供しています。 これにあたって、認証基盤の使い方やAPI仕様を社内で展開するためにDocusaurusを利用していますが、これまではDocusaurusのプラグインであるProtobuffetを利用してAPIドキュメントを自動生成していました。

Protobufのスキーマ情報からAPIドキュメントを生成する方法としては、protoc-gen-docを利用する方法がよく紹介されます。 protoc-gen-docは、protoファイルからMarkdownやHTML・JSONを生成するプラグインです。 このプラグインは、単体で成立するHTMLやMarkdownを出力できるため、カジュアルに使いやすいです。 しかし、単体では複数ページに構造化されたUIを生成できるわけではないので中規模以上のプロジェクトではそのまま利用するのは難しいでしょう。

Protobuffetであれば、 こちらのexampleのようにDocusaurusのドキュメント内にサイドバーを生成してくれるため、protoファイルごとのドキュメントを簡単に行き来できます。 そのため、中〜大規模なプロジェクトでもprotoc-gen-docよりも使いやすいと考え採用していました。

Protobuffetで生成したドキュメント

APIドキュメントに求めるもの

Protobuffetを利用しDocusaurus上でAPIドキュメントを提供していましたが、いくつかの課題があり、APIドキュメントにおいて次のような内容が必要だと考えました。

  1. どんなAPIが存在するか分かりやすい
  2. APIに必要なリクエストの内容や制約がわかる

ドキュメントを確認する時、どんなAPIが用意されているか、リクエスト・レスポンスの内容はどういった内容かを知りたいはずです。 そうすると、ドキュメントにはまず見える位置にProtobufのServiceにどんなRPCが定義されているかが表示されて欲しいですし、そのRPCで利用するメッセージ定義は近くに表示されて欲しいです。
しかし、Protobuffetではファイルごとに生成されたページの中で MessageEnumService という順番で定義が並びます。 そのため、特にProtocol Buffersに慣れていない人がドキュメントを見た場合、何に使うのかわからないメッセージ定義ばかりが目に入ることになってしまいます。 どんな機能があるのかを把握しづらいですし、RPC定義とメッセージ定義を行ったり来たりする必要があります。
また、ドキュメントを確認する開発者は、そのメッセージ定義の属性がどんな制約を持っているのかも知りたいはずです。 Protobuffetではprotoc-gen-validateProtovalidate 1の情報を反映してくれるわけではないので、descriptionなどで明記しない限り制約を開発者に伝えられません。

ProtobufスキーマをOpenAPIスキーマに変換するというアプローチ

上述の通り、Protobufスキーマから直接ドキュメントを生成する方法ではページの構成などが洗練されていないなどの課題がありました。ドキュメント生成にBuf Schema Registry 2 を利用する選択肢もありますが、コストがかかってしまいます。
そこで、ProtobufスキーマからOpenAPIスキーマに変換する方法を検討しました。 OpenAPIであれば、ドキュメントの生成方法もProtobufのエコシステムよりも多くの選択肢がありますし、上記の要求を満たす期待があったからです。

protoc-gen-connect-openapi の紹介

protoc-gen-connect-openapi は、ProtobufスキーマをOpenAPIスキーマに変換するプラグインであり、Connect RPCに対応しています。 また、このプラグインはOpenAPI v3.1に対応しているだけでなく、Protovalidateに対応しており、リクエストパラメータの制約をドキュメントに反映できます。 さらに、google/gnosticを利用してOpenAPI特有の情報をProtobufのoptionとして埋め込むこともできます!

こちらがprotoc-gen-connect-openapiRedocを利用して生成されたドキュメントのサンプルになります。 RedocusaurusというDocusaurusのプラグインを利用しています。

protoc-gen-connect-openapiで生成したドキュメント

ProtobufスキーマをOpenAPIスキーマに変換する方法は、gRPC Ecosystemのprotoc-gen-openapiv2を利用する方法やsolo-io/protoc-gen-openapiといったプラグインを利用する方法が存在します。 しかし、OpenAPI v3に未対応であったりProtovalidateに対応していなかったりと満足できるものはありませんでした。 このプラグインを利用することで どんなAPIが存在するか分かりやすいAPIに必要なリクエストの内容や制約がわかるといったことを満たせるようになりました。

上記のスクショを見ていただくと、RPCで利用するリクエスト・レスポンス内容はわかるが、どのmessageに対応するかわからないのでは、という声が聞こえそうです。 実際にAPIを利用する場合は、Protobufスキーマからクライアントコードを生成して利用してもらうことを想定しているため、LSPがRPCで利用するmessageを補完してくれることを期待しています。 また、Buf CLIを利用していればRPCで利用するmessageの命名規則が強制されるため、それに従っているのであればリクエスト・レスポンスのmessageは容易に推測可能です。 そのため、messageの名前はなくても困ることはないだろうと考えています。

protoc-gen-connect-openapiを利用する上で工夫したこと

ここからは、実際にprotoc-gen-connect-openapiを利用してドキュメントを生成する際の工夫についてご紹介します。

protoc-gen-connect-openapi の設定

こちらが、生成に利用するbuf.gen.yamlの例になります。
Connect RPCを利用していることもあり、ProtobufのプラグインはBuf CLI3を利用して呼び出しています。

version: v2
clean: true
plugins:
  - local: protoc-gen-connect-openapi
    strategy: all
    out: ./gen
    opt:
      - path=example-api.yaml
      - short-service-tags
      - with-streaming
inputs:
  - directory: ../proto

buf.gen.yaml

  1. short-service-tags を指定する
    protoc-gen-connect-openapi ではProtobufスキーマをOpenAPIスキーマに変換する際、パスごとに元々定義されていたServiceをtags属性に設定してくれます。 そのため、RedocなどでServiceごとにグループ化して表示できます。 この際、デフォルトではstore.v1.StoreServiceといったProtobufスキーマのパッケージ名を含む名前が利用されます。 パッケージ構成的にサービス名が被らないのであれば、パッケージ名はない方が見やすいです。 パッケージ名が不要な場合は、short-service-tagsを指定してあげると良いでしょう。

    short-service-tagsを指定しなかった場合

  2. with-streaming を指定する
    デフォルトでは、Streaming RPCは生成の対象外となっています。 Streamingを含むRPCもドキュメント化の対象にしたい場合は、 with-streamingを指定してください。

そのほかにもConnect RPC向けの機能を無効化したりなど、豊富なオプションが用意されています。 詳しくは、 こちらをご覧ください。

Protobufスキーマの定義にOpenAPI特有の情報を埋め込む

protoc-gen-connect-openapiでは、デフォルトでProtobufスキーマのRPCやmessageに付けられたコードコメントをOpenAPIのdescription属性へ変換してくれます。 ただし、RPCにつけたコメントはdescription属性とsummary属性の両方に設定されてしまいます。 我々の場合は、元々Protobuffetのドキュメント生成のためにdescriptionとして表示されることを前提とした内容をコメントとして残していました。 これが、summary属性として扱われてしまうとRedocなどで生成したドキュメントが見づらくなってしまいます。 protoc-gen-connect-openapiでは、google/gnosticが提供している4情報を利用することでProtobufスキーマにOpenAPI特有の情報を付与できます。

我々の場合は、上述の問題を解決するために以下のような形でsummary属性をgnosticのoptionを利用して付与しています。

syntax = "proto3";

package store.v1;

import "buf/validate/validate.proto";
import "gnostic/openapi/v3/annotations.proto";
import "google/protobuf/empty.proto";

// StoreService provides operations for managing retail stores.
// This service allows creating, reading, updating, and deleting store information.
service StoreService {
  // GetStore retrieves a specific store by its ID.
  rpc GetStore(GetStoreRequest) returns (Store) {
    option (gnostic.openapi.v3.operation).summary = "Get Store";
  }
  // ListStores retrieves a list of stores with pagination support.
  rpc ListStores(ListStoresRequest) returns (ListStoresResponse) {
    option (gnostic.openapi.v3.operation).summary = "List Stores";
  }
  // CreateStore creates a new store with the provided information.
  rpc CreateStore(CreateStoreRequest) returns (Store) {
    option (gnostic.openapi.v3.operation).summary = "Create Store";
  }
  // UpdateStore updates an existing store's information.
  rpc UpdateStore(UpdateStoreRequest) returns (Store) {
    option (gnostic.openapi.v3.operation).summary = "Update Store";
  }
  // DeleteStore deletes a store by its ID.
  rpc DeleteStore(DeleteStoreRequest) returns (google.protobuf.Empty) {
    option (gnostic.openapi.v3.operation).summary = "Delete Store";
  }
}

protoファイルに書きづらい情報は独立したyamlに定義する

OpenAPIスキーマでは、トップレベルのdescriptionにMarkdownを記載してAPI利用にまつわる情報を記述することが多いでしょう。 例えば、次のような内容を用意してみます。

openapi: 3.0.0
info:
  title: Hello World
  version: v2
  description: |
    # Hello World Service

    This is a service which says hello to you!

    ## Features

    - **Personalized Greetings**: Send customized hello messages with different names
    - **Simple Integration**: Lightweight and easy to integrate into your applications
    - **Flexible Response**: Receive friendly responses tailored to your input

    ## Getting Started

    To use this service, simply call the appropriate endpoint with your desired name parameter.
    The service will return a personalized greeting message.

    ### Example

    ``json
    {
      "name": "John"
    }
    ```

    Will return a friendly hello message addressed to John.
  contact:
    name: John Doe
    url: https://github.com/
    email: john@example.com
components:
  securitySchemes:
    BasicAuth:
      type: http
      scheme: basic

前述の通り、protoc-gen-connect-openapiでOpenAPIスキーマを生成する場合でもgnosticの定義を利用することでdescriptionなどを記載できます。 しかし、ProtobufではHere Documentのような機能がサポートされていません。 そのため、仮にProtobufスキーマのなかにinfo属性を埋め込もうとすると次のようになってしまいます。 文字列リテラルの中で改行などをエスケープしながら複数行にわたるMarkdownをメンテナンスしていくことは困難でしょう。

syntax = "proto3";

package example.v1.hello;

import "buf/validate/validate.proto";
import "gnostic/openapi/v3/annotations.proto";

option (gnostic.openapi.v3.document) = {
  info: {
    title: "Hello World"
    version: "v2"
    description: "# Hello World Service\n\nThis is a service which says hello to you!\n\n## Features\n\n- **Personalized Greetings**: Send customized hello messages with different names\n- **Simple Integration**: Lightweight and easy to integrate into your applications\n- **Flexible Response**: Receive friendly responses tailored to your input\n\n## Getting Started\n\nTo use this service, simply call the appropriate endpoint with your desired name parameter.\nThe service will return a personalized greeting message.\n\n### Example\n\n```json\n{\n  \"name\": \"John\"\n}\n```\n\nWill return a friendly hello message addressed to John."
    contact: {
      name: "John Doe"
      url: "https://github.com/"
      email: "john@example.com"
    }
  }
  components: {
    security_schemes: {
      additional_properties: [
        {
          name: "BasicAuth"
          value: {
            security_scheme: {
              type: "http"
              scheme: "basic"
            }
          }
        }
      ]
    }
  }
};

そのため今回は、トップレベルのdescriptionなど一部の項目は独立したyamlに定義しておき、ドキュメント生成前に結合する手段を取りました。

Redocly CLIには、experimentalではあるものの複数のOpenAPIスキーマをひとつに結合するためのjoinコマンドが用意されています。 これを利用することで、yamlの属性をマージするようなスクリプトを自前で用意せずにさくっとスキーマを結合できます。

最終的なドキュメント生成の流れとしては次のようになりました。

  1. $ buf generate
    まずは、bufコマンドでprotoc-gen-connect-openapiを動かしてProtobufスキーマからOpenAPIスキーマを生成します。 ここで生成されたファイルをexample-api.yamlとします。
  2. $ redocly join general-info.yaml example-api.yaml -o result.yaml
    OpenAPIスキーマのinfo属性などはgeneral-info.yamlに定義しておきます。 Redocly CLIを利用して①で生成したexample-api.yamlgeneral-info.yamlをマージします。
  3. $ docusaurus build
    最後にDocusaurusをビルドして、ドキュメントを生成します。
    この際、Redocusaurusの設定で②で生成したスキーマ情報(result.yaml)を利用するようにしておきます。

まとめ

protoc-gen-connect-openapiを利用することで、ProtobufスキーマからProtovalidateで定義した制約の内容なども反映したOpenAPI v3スキーマを生成することができました。 これによって、OpenAPIのエコシステムを利用して見やすいAPIドキュメントを生成し、簡単にDocusaurusへ組み込めました。 これまで、ProtobufからAPIドキュメントを生成するにあたって課題感があり、ドキュメント生成を自作することも考えていました。 しかし、protoc-gen-connect-openapiがよくできていたため、自作せずに済みました。

また、今回初めてRedocを触ったのですが、生成したドキュメントが見やすいだけでなくCLIなどのエコシステムが整っており魅力を感じました。 今回の方法が、これまでprotoc-gen-docやProtobuffetを利用していた方の参考になれば幸いです。

最後になりますが、Platform EUで取り組んでいることについてこちらでお話ししていますので、ぜひご覧ください。 jp.corp-sansan.com


  1. protoc-gen-validateは、Protobufメッセージのバリデーションルールを定義するためのライブラリです。既にメンテナンスモードになっており、機能追加は行われていません。Protovalidateは、その後継として開発されたモダンなバリデーションライブラリで、より柔軟な検証ルールの記述と、複数の言語での一貫したバリデーション実装を提供しています。
  2. Buf Schema Registry(BSR)は、Connect RPCなどのエコシステムを開発しているBuf社が提供しているProtobuf用のスキーマレジストリです。BSR上に保存したスキーマ情報から、ブラウザで閲覧できるAPIドキュメントを自動生成してくれる機能も搭載しています。
  3. Buf CLIとは、Protobufのビルド、リント、フォーマット、依存関係管理などを行うためのCLIです。従来のprotocコマンドよりも高速で使いやすく、モダンなProtobuf開発において広く採用されています。
  4. gnosticとは、OpenAPIスキーマとProtobufスキーマを相互に変換するツールなどを含むプロジェクトです。gnostic/openapi/v3/annotations.protoを使用することで、OpenAPIの情報(title、description、contact情報など)をProtobufスキーマのメタ情報として記述できます。

© Sansan, Inc.