スキーマ定義言語 Protocol Buffers と protoc-gen-swagger を使って Web API のスキマを埋めよう

VOYAGE Lighthouse Studio の海老原 (@co3k) です。先日 30 歳になった記念としてタイトルはオヤジギャグです。

さて、普段は 神ゲー攻略 というゲーム攻略サイトを運営しているのですが、とある派生サービスを立ち上げるにあたり、 Web API スキーマ定義を gRPC に基づく形式の Protocol Buffers で書き、 protoc-gen-swagger プラグインを介して OpenAPI 定義ファイルとして生成する、というアプローチを採りました。

yugui さんの素晴らしい記事、「今さらProtocol Buffersと、手に馴染む道具の話」によってスキーマ定義言語としての Protocol Buffers がにわかに注目を浴びて以降、似たようなことをやりたいという方もいらっしゃるのではないでしょうか。

ところが、おそらく単体で protoc-gen-swagger プラグインを使う人はまだまだ限られているようで、

  • 機能が充分でなかったり、不安定であったりする
    • 複数スキーマのマージやエラーレスポンスの定義が割と最近サポートされた
    • 「デフォルト認証状態のリセット」が最近までできなかったうえに segfault で落ちていた
  • ドキュメントが不足している

という具合に、ちょっとまだ導入にあたってハードルが高めなのが正直なところです。

ただ、悪い話ばかりでもありません。ここでひとつ朗報なのですが、 2018/09/09 にリリースされた v1.5.0 では、前者の問題を解決するための変更が多く取り込まれています。

そのうちいくつかは、海老原がサービス開発中に必要となったものを修正し、取り込んでもらったものです。以下がその pull request です。

また、もちろん、他の方がおこなっておられる変更にも、 protoc-gen-swagger 単体で用いる際に有用となるものがあります。たとえば以下のようなものです。

しかしドキュメントは相変わらずありません!

そういったわけで、本エントリではこの選択をした理由のご紹介と、 v1.5.0 の新機能なども含む protoc-gen-swagger の使い方について簡単に説明していきます。同じようなアプローチを取る方の一助になれば幸いです。

TL;DR

読者の便宜のために、本エントリでご紹介する .proto ファイルと .swagger.json ファイルの例、および生成のための Makefile をまとめたリポジトリをご用意しました。ぜひご活用ください。 https://github.com/co3k/protobuf-swagger-example/

このアプローチを採った理由

  • JSON Hyper-Schema を書くのは (JSON を書かなかったとしても) 正直つらい
  • REST API をやっていくならエコシステム的に Swagger (と OpenAPI) 1 一強といった状況である
  • OpenAPI 仕様ではスキーマ部の定義に JSON Schema を記述することになるが、これも正直つらい
  • あっ、ちょうどいい感じのスキーマ定義言語として Protocol Buffers があるじゃん!
  • しかも protoc が思ったよりも強力じゃん! 知らなかった!
  • しかもしかも grpc-gateway をよく見たら protoc-gen-swagger なるプラグインがあるじゃん!

そもそも海老原は 4 年ほど前の LT、「JSON Schema で Web API のスキマを埋めよう」で触れているように (タイトルはダジャレです)、JSON Hyper-Schema による Web API スキーマ定義をこれまでずっと続けていました。これは JSON Hyper-Schema そのものにアドバンテージを感じていたというより、何でもいいので何らかのスキーマ定義を必要としていたことと、必要に迫られて contribute もした Heroku 製の prmd というドキュメント生成ツールを気に入っていたから、という側面が強いです。

正直 JSON Hyper-Schema や JSON Schema が気に入っていたかというとそんなことはなくて、かなり無理して書いていた感が強いです。もちろん JSON は human-writable ではないので YAML で書くわけですが、 JSON も YAML も汎用的なフォーマットであるがゆえに、どうしても持ってまわった書き方にならざるを得ないところがあります。

百聞は一見にしかずということで、 VOYAGE GROUP のバースペース AJITO の T シャツ裏に印字された JSON のスキーマ定義を考えてみましょう。印字された JSON は以下に 2 示す 3 とおり 4 です。 AJITO で過ごすことを ajiting と呼んでいるのですが、その ajiting とはどういったものか、というのを紹介するような内容となっています。

{ "#ajiting": {
    "description": "coding, discussion and other.",
    "url": "http://ajito.vg",
    "location": {
      "longitude": "35.6553195",
      "latitude": "139.6937795"
    },
    "beer": "free"
  }
}

この JSON 表現のスキーマを JSON Scheme によって表現してみると、たとえば以下のようになるでしょうか。

{
  "type": "object",
  "properties": {
    "#ajiting": {
      "type": "object",
      "properties" : {
        "description": {
          "type": "string"
        },
        "url": {
          "type": "string"
        }
        "location": {
          "type": "object",
          "properties": {
            "longitude": "string",
            "latitude": "string"
          }
        },
        "beer": "string"
      }
    }
  }
}

対して、 Protocol Buffers の場合はそもそもスキーマ定義を目的として作られたフォーマットであるため、簡潔な記述で済みます。

syntax = "proto3";

message Ajiting {
  message GeoCoordinate {
    string latitude = 1;
    string longitude = 2;
  }

  string description = 1;
  string url = 2;
  GeoCoordinate location = 3;
  string beer = 4;
}

message AjitingResponse {
  Ajiting ajiting_message = 1 [json_name = "#ajiting"];
}

さっそく Web API スキーマ定義を書いてみよう!

前置きはここまでとして、さっそく WebAPI のスキーマ定義を書いてみましょう。

ここで必要となってくるのが Protocol Buffers に関する以下のような知識です。

  • message の定義に関する知識
  • service の定義に関する知識
  • option に関する知識

これらについて理解しておけば、簡単な Web API 定義を書くには充分です。順番に見ていきましょう。

message の定義に関する知識

さて、リクエストやレスポンスなどを表現する message の記法については既に示したとおりです。詳しくは Language Guide を通読していただくとして、肝心なところだけ拾って説明していきます。

まずは = 1 などの謎の代入文についてですが、これは「タグ」と呼ばれるもので、フィールドを一意に識別するための番号です。が、最終的に JSON シリアライズするうえでは重要でない概念なので、とにかく 1 から順番に機械的につけていけばよい、と覚えておいてください。

また、 message は入れ子にすることができます。以下のような形です。

message Ajiting {
  message GeoCoordinate {
    string latitude = 1;
    string longitude = 2;
  }

  GeoCoordinate location = 3;
}

無名 message のようなものを定義したくなるところですがそれはできません。これでも JSON Schema に比べればまだシンプルなので、ここは名前付けの機会をもらえたと思ってグッとこらえましょう 5

それから忘れてはならないのは配列表現でしょうか。 AJITO T シャツに印字された JSON には以下のようなプロパティが存在します。

"beer": "free"

しかしこれは実に遠慮がちな表現で、 ajiting において free なのは beer だけではありません。せっかくなので Protocol Buffers における配列表現を用いつつ実態に合わせて修正してみましょう。

Protocol Buffers には repeated フィールドがあります。これは JSON シリアライズした場合に配列表現となります。

というわけで Ajiting の定義から beer を取り除き、文字列値の配列を表す以下の記述で置き換えます。

repeated string free = 5;

これによって、以下のような表現が合法となりました。よかったですね 6

"free": [
  "beer", "cocktail", "non-alcoholic cocktail", "juice",
  "talking", "coding", "playing instruments"
]

Web APi スキーマ定義用途であれば、 message について知っておくべきことはこれだけです。あとは 基本的な型 を確認しながら書いていきましょう。

service の定義に関する知識

続いて service の定義についてです。これは RPC 7 におけるメソッド定義の集合です……という説明より、実際にやってみたほうが多分早いですね。

ではさっそく何か service とメソッドを定義してみましょう。「ajiting とは心の所作」とはよく言ったもので、人にとって様々な ajiting があります。議論の場としての ajiting、作業スペースとしての ajiting、娯楽の場としての ajiting、 AJITO 以外での ajiting――ということで、全世界のいろいろな ajiting を一覧するメソッドがあれば便利そうですね。

まずは空の service を定義します。

service AjitingService {
}

次に、この service にメソッドを定義します。

service AjitingService {
  rpc ListAjiting(ListAjitingRequest) returns (ListAjitingResponse);
}

rpc に続く文字列がメソッド名です。続く括弧の中身がリクエストとなる message で、 returns に後続する括弧の中身がレスポンスとなる message です。もちろん ListAjitingRequest も ListAjitingResponse もまだ定義していないのでここで一緒に定義してしまいましょう。

まずリクエストについてですが、条件に合った ajiting の一覧が取得できたら便利そうではないでしょうか。ということで、検索クエリを指定できるような感じのメッセージを考えてみます。

message ListAjitingRequest {
  string query = 1;
}

レスポンスは Ajiting の配列と、あとは全件数でも返しておきましょうか。

message ListAjitingResponse {
  int32 num = 1;
  repeated Ajiting ajitings = 2;
}

基本的な service 定義についてはここまでの知識で充分です。

ちなみに service をどういう単位で作っていくか、というところですが、いろいろ試してみた感じ REST でいうところのリソース単位で細かく区切っていくと収まりが良さそうでした。

それからメソッドの命名については Google Cloud APIs の API Design Guide 内 Standard Methods の命名規則にとりあえず従ってつけています。この辺はお好みですが、いずれにせよある程度の一貫性があるといいですね。

option と google.api.http に関する知識

ここまでの知識だけではまだ Web API スキーマ定義を作ることはできません。ほとんどの場合、 message については特別な考慮をすることなく JSON にシリアライズ可能ですが、 Protocol Buffers でいうところの service と、 REST の文脈でいうところのリソースとメソッドという概念が結びついていません。

そうは言っても、もちろん、 service の概念を REST で表現したいというのは Protocol Buffers そのものがカバーする領域ではありません。こういうときに活躍するのが option です。これは端的にいうとファイル、 message やそのフィールド、 service やそのメソッドなどに対して任意のメタ情報を付加できる仕組みです。この仕組みの存在が、 Protocol Buffers をシンプルでありながらもパワフルな武器として成立させています。

protoc-gen-swagger (と、 grpc-gateway) は google.api.http 型のメッセージをメソッドに対する option として解釈できます。

まずはこのメッセージの定義をファイルの先頭あたりで import します。

import "google/api/annotations.proto";

そのうえで、たとえば先程の ListAjiting(ListAjitingRequest) に対して GET /v1/ajiting をマップするのであれば、以下のようにします。

rpc ListAjiting(ListAjitingRequest) returns (ListAjitingResponse) {
  option (google.api.http).get = "/v1/ajiting";
}

ListAjitingRequest には query というフィールドがありますが、デフォルトではすべてのフィールドはクエリパラメータとして扱われます。

もしパスパラメータとして扱いたい場合は、 (google.api.http).get の文字列に URI Template でお馴染みの形式で含んであげればよいです。パスパラメータに記述したフィールドは、クエリパラメータとして扱われなくなります。

rpc ListAjiting(ListAjitingRequest) returns (ListAjitingResponse) {
  option (google.api.http).get = "/v1/ajiting/{query}";
}

GET 以外のメソッドの場合も見てみましょう。既存の ajiting を PUT で編集する以下のメソッドを考えます。

rpc UpdateAjiting(UpdateAjitingRequest) returns (AjitingResponse);

UpdateAjitingRequest の定義は以下のようになるでしょうか。

message UpdateAjitingRequest {
  int32 id = 1;
  string description = 2;
  string url = 3;
  Ajiting.GeoCoordinate location = 4;
  repeated string free = 5;
}

では REST における PUT メソッドの定義を書いていきます。

rpc UpdateAjiting(UpdateAjitingRequest) returns (AjitingResponse) {
  option (google.api.http) = {
    put: "/v1/ajiting/{id}"
    body: "*"
  }
}

GET のときは違い、パスパラメータに使われなかったフィールドは、そのままではリクエストボディとして扱われません。そのため、残りのフィールドをすべてリクエストボディに含むよう、 body: "*" を指定しています。この指定がない場合はリクエストボディが空であるとみなされます。

ここで、 * 以外を指定することもできます。 UpdateAjitingRequest を以下のように変えて、 Ajiting を再利用するようにしてみましょう。

message UpdateAjitingRequest {
  int32 id = 1;
  Ajiting ajiting = 2;
}

こうしておけば、以下のように書けます。

rpc UpdateAjiting(UpdateAjitingRequest) returns (AjitingResponse) {
  option (google.api.http) = {
    put: "/v1/ajiting/{id}"
    body: "ajiting"
  };
}

OpenAPI 定義ファイルの生成

ここまでで Web API を表す Protocol Buffers 定義が出来上がりました。このファイルを ajiting.proto として保存しておきましょう。

syntax = "proto3";

import "google/api/annotations.proto";

message Ajiting {
  message GeoCoordinate {
    string latitude = 1;
    string longitude = 2;
  }

  string description = 1;
  string url = 2;
  GeoCoordinate location = 3;
  repeated string free = 5;
}

message AjitingResponse {
  Ajiting ajiting_message = 1;
}

message ListAjitingRequest {
  string query = 1;
}

message ListAjitingResponse {
  int32 num = 1;
  repeated Ajiting ajitings = 2;
}

message UpdateAjitingRequest {
  int32 id = 1;
  Ajiting ajiting = 2;
}

service AjitingService {
  rpc ListAjiting(ListAjitingRequest) returns (ListAjitingResponse) {
    option (google.api.http).get = "/v1/ajiting";
  }

  rpc UpdateAjiting(UpdateAjitingRequest) returns (AjitingResponse) {
    option (google.api.http) = {
      put: "/v1/ajiting/{id}"
      body: "ajiting"
    };
  }
}

ではさっそく protoc-gen-swagger で OpenAPI 定義ファイルを生成します。

まず Protocol Buffers (と protoc) をインストール したうえで、

$ go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger

によって protoc-gen-swagger を入手します。

あとは以下のコマンドを叩くだけ 8

$ protoc -I. -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/ --swagger_out=json_names_for_fields=true:. ajiting.proto

これで ajiting.swagger.json が生成されます。できあがったものは https://github.com/co3k/protobuf-swagger-example/blob/ce7a439ef0c692388549d3e18ab796bb3f46d5e7/01-ajiting.swagger.json に置いたので、 https://editor.swagger.io/ などでご確認ください。なかなかいい感じではないでしょうか?

ちなみに、 v1.4.1 から、複数の .proto ファイル定義を単一の .swagger.json ファイルにまとめられるようになりました。以下のように allow_merge パラメータに true を指定するだけです。便利!

$ protoc -I. -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/ --swagger_out=json_names_for_fields=true,allow_merge=true:. *.proto

OpenAPI 特有の設定をガッツリ書いていこう!

最小限のスキーマ定義はもうここまでで充分なわけですが、 Swagger のエコシステムをフル活用しようとすると、まだ物足りないところもあります。認証関連の設定であったり、ドキュメント生成やバリデーションなどですね。 protoc-gen-swagger はこのあたりもバッチリサポートしています。

そういった場合に必要な OpenAPI 特有の設定を Protocol Buffers 上で表現するのに、 option が大活躍するわけです。 protoc-gen-swagger が細かい設定類を定義するためのメッセージ群を用意してくれている ので、これを使っていきましょう 9

Swagger オブジェクト (ルートオブジェクト) の定義

一番外側のスコープで option を記述すると、ファイルレベルのメタ情報を付加することができます。

protoc-gen-swagger が提供する Swagger メッセージ (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) によって、 OpenAPI 仕様のルートオブジェクトである Swagger オブジェクトへの拡張をおこなうことができます。

まずは必要なファイルを import して、

import "protoc-gen-swagger/options/annotations.proto";

以下のように記述します。

option (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) = {
    swagger: "2.0";
    info: {
        title: "ajiting-api";
        description: "ajiting 用の Web API です。";
        version: "1.0";
    }
    host: "api.ajiting.example.com";
    schemes: HTTPS;
    security_definitions: {
        security: {
            key: "OAuth2";
            value: {
                type: TYPE_OAUTH2;
                flow: FLOW_ACCESS_CODE;
                authorization_url: "https://ajiting.example.com/oauth/authorize";
                token_url: "https://ajiting.example.com/oauth/token";
            }
        }
    }
    security: {
        security_requirement: {
            key: "OAuth2";
            value: {
            };
        }
    }
    responses: {
      key: "403";
      value: {
        description: "リソースへのアクセス権がない場合のレスポンスです。";
      }
    }
    responses: {
      key: "404";
      value: {
        description: "リソースが見つからなかったときのレスポンスです。";
      }
    }
};

はい、見てのとおり OpenAPI の Swagger オブジェクトをほぼそのまま Protocol Buffers として定義し直したような形になっているので、詳しいことは https://github.com/OAI/OpenAPI-Specification/blob/3.0.0/versions/2.0.md#swagger-object を見ながら設定していただければよろしいかと思います。

細かい注意点としては、

  • Protocol Buffers 側のキーはスネークケースで書く必要があります
  • 特定の値しか許容しない schemessecurity_definitionstype などは enum が定義されていて、その値を使っていく形になります。このあたりのドキュメントは存在しないので、 openapiv2.proto を見ながらどのような型を受け容れるのかを確認していく必要があります
  • reserverd キーワードで予約されているフィールド (Swagger オブジェクトであれば paths, definitions, tags) には未対応です

といったあたりでしょうか。

Operation オブジェクトの定義

OpenAPI における Operation は Protocol Buffers のメソッドに対応します。メソッドに対して grpc.gateway.protoc_gen_swagger.options.openapiv2_operation の option を設定することで、この Operation を拡張できます。以下は先程定義した AjitingService の ListAjiting の認証設定を上書き (ファイルレベルでは OAuth2 を強制するが、 ListAjiting は認証なしでアクセスできるようにする) してみましょう。

service AjitingService {
  // みんなの #ajiting を一覧する
  //
  // みんながそれぞれに思う #ajiting を一覧します。
  // query が指定されている場合はその条件に従った #ajiting を絞り込みます。
  rpc ListAjiting(ListAjitingRequest) returns (ListAjitingResponse) {
    option (google.api.http).get = "/v1/ajiting";
    option (grpc.gateway.protoc_gen_swagger.options.openapiv2_operation) = {
      security: {};  // ここでデフォルトの認証設定を上書きする
    };
  }
}

OpenAPI において、 Operation の security は Security Requirement Object の配列です。ファイルレベルで定義した認証設定とは別な認証設定を渡すことができるわけですが、ここで空配列を指定することで、認証設定を取り除く、つまり認証なしでのアクセスが許可されるようになります。

また、メソッドに対するコメントは、一行目が OpenAPI における Operation の summary として、空行を挟んでそれ以降の文字列が description として扱われます。最終的に OpenAPI 定義からドキュメント等を生成したい場合などに有用でしょう。

Schema オブジェクトの定義

OpenAPI における Schema は Protocol Buffers の message に対応します。こちらも grpc.gateway.protoc_gen_swagger.options.openapiv2_schema によって拡張可能です。

// 緯度経度情報
//
// 世界測地系 (WGS84) における緯度経度情報を表します。
// 日本測地系 2000 や 日本測地系 2011 などの他の測地系の値は受け容れませんので、
// 事前に変換をおこなっておく必要があります。
message GeoCoordinate {
  option (grpc.gateway.protoc_gen_swagger.options.openapiv2_schema) = {
    json_schema: {
      required: ["latitude", "longitude"]
    }
  };

  string latitude = 1;
  string longitude = 2;
}

ここでは、 json_schema によって JSON Schema を定義できます。なんだか本末転倒な気もしますが、いまのところ細かいバリデーションや required については JSON Schema 経由で定義していくしかありません。

message に対するコメントは、メソッドに対するコメントと同様に、 OpenAPI 定義における Schema の title ないし description として扱われます。

フィールドに対する JSON Schema Validation 定義を追加する

OpenAPI 定義上で利用できる JSON Schema Validation は、たとえば文字列のパターンを制限したい場合や、文字数制限をおこなう場合などに有用でした。

これを Protocol Buffers 上でも定義しましょう。フィールドに対するオプション grpc.gateway.protoc_gen_swagger.options.openapiv2_field を用いることで、以下のように表現できます。

string latitude = 1 [(grpc.gateway.protoc_gen_swagger.options.openapiv2_field) = {pattern: "^[\\-]?[0-9]{0,2}\\.[0-9]+$"}];
string longitude = 2 [(grpc.gateway.protoc_gen_swagger.options.openapiv2_field) = {pattern: "^[\\-]?[0-9]{0,3}\\.[0-9]+$"}];

特定のステータスコードのレスポンスを定義する

Swagger オブジェクトや Operation オブジェクトは responses を受け容れます。これによって特定のステータスコードのレスポンスを定義することができます。 Swagger オブジェクトに対する例を再掲します。

option (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) = {
    responses: {
      key: "403";
      value: {
        description: "リソースへのアクセス権がない場合のレスポンスです。";
      }
    }
    responses: {
      key: "404";
      value: {
        description: "リソースが見つからなかったときのレスポンスです。";
      }
    }
};

この例は説明を付加しただけでレスポンス自体の定義はおこなっていません。 RFC7807 に従ったエラーレスポンスを以下のように Protocol Buffers で定義します。

message ErrorResponse {
  string type = 1;
  int32 status = 2;
  string title = 3;
  string detail = 4;
  string instance = 5;
}

JSON Schema からはこれを以下のように参照できます。

responses: {
  key: "403";
  value: {
    description: "リソースへのアクセス権がない場合のレスポンスです。";
    schema: {
      json_schema: {
        ref: ".ErrorResponse";
      }
    }
  }
}

ガッツリ書いた結果を生成してみよう!

ということで、最終的な .proto ファイルは以下のような形になりました。実際には Swagger オブジェクトの定義や ErrorResponse などは独立した .proto に分けたいところですね。

syntax = "proto3";

import "google/api/annotations.proto";
import "protoc-gen-swagger/options/annotations.proto";

option (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) = {
    info: {
        title: "ajiting-api";
        description: "ajiting 用の Web API です。";
        version: "1.0";
    }
    host: "api.ajiting.example.com";
    schemes: HTTPS;
    security_definitions: {
        security: {
            key: "OAuth2";
            value: {
                type: TYPE_OAUTH2;
                flow: FLOW_ACCESS_CODE;
                authorization_url: "https://ajiting.example.com/oauth/authorize";
                token_url: "https://ajiting.example.com/oauth/token";
            }
        }
    }
    security: {
        security_requirement: {
            key: "OAuth2";
            value: {
            };
        }
    }
    responses: {
      key: "403";
      value: {
        description: "リソースへのアクセス権がない場合のレスポンスです。";
        schema: {
          json_schema: {
            ref: ".ErrorResponse";
          }
        }
      }
    }
    responses: {
      key: "404";
      value: {
        description: "リソースが見つからなかったときのレスポンスです。";
        schema: {
          json_schema: {
            ref: ".ErrorResponse";
          }
        }
      }
    }
};

// エラーレスポンスオブジェクト
//
// [RFC7807](https://tools.ietf.org/html/rfc7807) に従ってエラーの内容を表します。
message ErrorResponse {
  string type = 1;
  int32 status = 2;
  string title = 3;
  string detail = 4;
  string instance = 5;
}

// ajiting を表現するオブジェクト
//
// ajiting の緯度経度情報や、どのような ajiting がおこなわれるか、何が free なのかを表します。
message Ajiting {
  // 緯度経度情報
  //
  // 世界測地系 (WGS84) における緯度経度情報を表します。
  // 日本測地系 2000 や 日本測地系 2011 などの他の測地系の値は受け容れませんので、
  // 事前に変換をおこなっておく必要があります。
  message GeoCoordinate {
    option (grpc.gateway.protoc_gen_swagger.options.openapiv2_schema) = {
      json_schema: {
        required: ["latitude", "longitude"]
      }
    };

    // 緯度
    //
    // 世界測地系 (WGS84) における緯度情報を表します。
    string latitude = 1 [(grpc.gateway.protoc_gen_swagger.options.openapiv2_field) = {pattern: "^[\\-]?[0-9]{0,2}\\.[0-9]+$"}];

    // 経度
    //
    // 世界測地系 (WGS84) における経度情報を表します。
    string longitude = 2 [(grpc.gateway.protoc_gen_swagger.options.openapiv2_field) = {pattern: "^[\\-]?[0-9]{0,3}\\.[0-9]+$"}];
  }

  // ajiting の説明
  string description = 1;

  // ajiting の URL
  string url = 2;

  // ajiting がおこなわれる場所
  GeoCoordinate location = 3;

  // 何が free な ajiting か
  repeated string free = 5;
}

// ajiting を返すためのレスポンス
//
// T シャツの裏に印字されているように #ajiting をキーとする `Ajiting` を返します
message AjitingResponse {
  Ajiting ajiting_message = 1 [json_name = "#ajiting"];
}

// ajiting 一覧取得用リクエスト
message ListAjitingRequest {
  // 希望する ajiting の条件を指定するための検索クエリ
  string query = 1;
}

// ajiting 一覧取得用レスポンス
message ListAjitingResponse {
  // 該当する ajiting の件数
  int32 num = 1;

  // 該当する ajiting の一覧
  repeated AjitingResponse ajitings = 2;
}

// ajiting 更新要リクエスト
message UpdateAjitingRequest {
  // 更新する ajiting の ID
  int32 id = 1;

  // 更新する ajiting オブジェクトの内容
  Ajiting ajiting = 2;
}

service AjitingService {
  // みんなの #ajiting を一覧する
  //
  // みんながそれぞれに思う #ajiting を一覧します。
  // query が指定されている場合はその条件に従った #ajiting を絞り込みます。
  rpc ListAjiting(ListAjitingRequest) returns (ListAjitingResponse) {
    option (google.api.http).get = "/v1/ajiting";
    option (grpc.gateway.protoc_gen_swagger.options.openapiv2_operation) = {
      security: {};
      responses: {
        key: "404";
        value: {
          description: "指定した条件に当てはまる ajiting がなかった場合に返すレスポンスです。";
        }
      };
    };
  }

  // #ajiting を更新する
  //
  // 指定した内容によって #ajiting を更新します。
  rpc UpdateAjiting(UpdateAjitingRequest) returns (AjitingResponse) {
    option (google.api.http) = {
      put: "/v1/ajiting/{id}"
      body: "ajiting"
    };
  }
}

ではこれで ajiting.swagger.json を生成してみましょう。もちろんできあがったものはこちらにございます! https://github.com/co3k/protobuf-swagger-example/blob/ce7a439ef0c692388549d3e18ab796bb3f46d5e7/02-ajiting.swagger.json

まとめ

  • grpc-gateway 付属の protoc プラグイン、 protoc-gen-swagger を用いて Protocol Buffers ファイルから OpenAPI 定義ファイルを生成する方法をご紹介しました
  • そのうえで必要となる基本的な Protocol Buffers の書き方と、 OpenAPI 定義に踏み込んだ発展的な option の利用方法をご紹介しました

弊チームでは生成した OpenAPI 定義を基に、クライアント (TypeScript) 側スクリプトを AutoRest で、サーバ (Go) 側スクリプトを go-swagger によって生成することでかなり楽をできている実感があります。みなさんにもぜひお試しいただければ幸いです。


  1. 本エントリでは、ツール等を含めた Swagger のエコシステムを含めて Swagger と呼び、 OpenAPI v2 仕様そのものへの言及については OpenAPI と呼んで区別することにします。なお、 protoc-gen-swagger はいまのところ OpenAPI v2 準拠なので、 OpenAPI v3 は本エントリのスコープ外です。

  2. 現在 VOYAGE GROUP は url フィールドに記載された http://ajito.vg を所有していませんのでご注意ください。

  3. 既報のとおり VOYAGE GROUP は 2019 年にオフィス移転を予定しており、 AJITO も生まれ変わります。 ajiting の際にはこの location フィールドの座標をアテにせず、 Web サイトに掲載された最新のアクセス情報 をご参照ください。

  4. この free は「言論の自由 (free speech)」ではなく「ビール飲み放題 (free beer)」の方の free です。

  5. 一応このように定義しておけば、外から Ajiting.GeoCoordinate として参照できるというメリットもあります。

  6. この free は「言論の自由 (free speech)」の free でもあり「ビール飲み放題 (free beer)」の free でもあります。

  7. もちろん gRPC も含みますがこれに限定されません。

  8. JSON スキーマ上で #ajiting というキー名を利用可能にするために json_names_for_fields オプションを指定しています。通常はあまり気にすることはないと思いますが、覚えておいて損はないオプションかもしれません。ちなみにこのオプションは v1.5.0 にて最近追加されたものです。

  9. なお、ここから説明することは本当にドキュメントがないので心して読んでください。