Protobuf から GraphQL コンバーター
.proto スキーマを貼り付けるだけ。type、enum、非 null リスト、map フィールド用の合成 entry 型が揃った GraphQL SDL を返します — ゲートウェイのスキーマの叩き台にそのまま使えます。
入力 (.proto スキーマ)
出力 (GraphQL SDL)
このツールでできること
Protocol Buffers のスキーマがあって、その前段に GraphQL ゲートウェイがある(あるいはこれから作る)状況だとします。Google の rejoiner のようなツールは proto メッセージをランタイムで GraphQL エンドポイントに繋ぎますが、スキーマの下書き、コードレビュー、手書きリゾルバ層の叩き台を作るときは、SDL がどう見えるかを確認したいだけのことが多いはず。このコンバーターはそれをブラウザで完結させます — 貼り付けて、出力をコピーして、schema.graphql に入れる、それだけです。
各 message は GraphQL の type になり、各 enum は GraphQL の enum になります。フィールド名は snake_case(proto の慣習)から camelCase(GraphQL 仕様に明記され、すべてのリンターが要求する慣習)に変換されます。スカラーと enum は非 null(String!、OrderStatus!)として出力されます — proto3 のフィールドはワイヤー上で常に値を持ち、未設定でもゼロ値が入るためです。単数の入れ子メッセージは nullable で出力され、proto3 の has-value セマンティクスと一致します。
repeated フィールドは [T!]! — 非 null 要素の非 null リスト — として出力されます。proto3 の repeated フィールドは null になることも null エントリを含むこともないからです。map には回避策が必要です。GraphQL にはネイティブな map 型が無いため、metadata フィールドの map<string, string> は [OrderMetadataEntry!]! となり、合成された type OrderMetadataEntry { key: String! value: String! } が付きます — Apollo や本番運用される proto-to-GraphQL ゲートウェイのほとんどが採用しているパターンと同じです。変換はすべてクライアントサイドで動き、スキーマがページ外に出ることはありません。
使い方
3 ステップ。出力は SDL を受け付けるどの GraphQL サーバーにもそのまま貼れます。
.proto スキーマを貼り付ける
左のエディターにスキーマを入れます。先頭の syntax = "proto3"; は任意です — パーサーは入れ子の message ブロック、enum 宣言、oneof、map<K, V>、フィールドオプションを扱います。import は認識しますがスキップするので、依存する型はインラインで貼ってください。
フィールド名の変換は自動です:.proto の order_id は GraphQL では orderId になります。型名と enum 名は PascalCase のままです。map フィールドには <親><フィールド>Entry という合成 entry 型が付きます。
出力を確認する
右側:GraphQL の SDL。enum が先、次に map の entry 型、その後に宣言順の message が並びます。入れ子の型は親より前に出るので、ファイルは上から下へ素直に読めます。graphql-tools や buildSchema でサーバーが読み込む .graphql ファイルに入れてください。
リゾルバを繋ぐ
スキーマで形は決まりますが、リゾルバは別途書く必要があります(または gRPC サービスから生成)。手早く動かしたいなら、この SDL とスタブリゾルバを Apollo Server に渡し、各スタブを gRPC バックエンドへの呼び出しに置き換えていきます。ランタイムの契約が proto3 の既定と異なる場合は nullable 設定を調整してください。
実際に時間を節約できる場面
gRPC バックエンドの上に GraphQL ゲートウェイを立ち上げる
チームには gRPC サービスがあり、プロダクトは Web クライアント向けに GraphQL エンドポイントを欲しがっている。リゾルバを書く前にフロントエンドエンジニアと議論するための叩き台のスキーマが必要 — proto を貼り付けて SDL をコピーしてドキュメントに落とすだけで完了。
Protobuf API 変更のレビュー
バックエンドの同僚が message にフィールドを足した。コード生成パイプライン全体を回し直さずに公開 GraphQL サーフェスへの影響を見たい。新しい .proto を貼って、SDL の出力を現スキーマと差分し、的を絞ったレビューコメントを残せます。
ドキュメントと設計議論
将来 GraphQL を前段に置く予定の新しい gRPC サービスについて RFC を書いている。proto と SDL の両方を文書に並べると議論が具体的になります。このコンバーターを使えば、ビルドを立ち上げずに SDL 側を用意できます。
REST や gRPC から GraphQL への移行
Protobuf で定義された既存サービスを引き継ぎ、新しいプロダクト要件で GraphQL API が求められた。ここで下書きスキーマを生成して移行の出発点にし、フィールド名・nullable・ページネーションを手で詰めていきます。
よくある質問
スキーマはどこかに送信されますか?
いいえ。.proto パーサーも SDL エミッターもブラウザ内の JavaScript として動きます。DevTools を開いて Network タブを見ながら貼り付けてみてください — リクエストはゼロです。スキーマに社内の型名やパッケージパスなど第三者サービスに送りたくない情報が含まれているときに役立ちます。
64 ビット整数が String になるのはなぜ?
GraphQL の組み込みスカラー Int は GraphQL 仕様により符号付き 32 ビットなので、proto の int64 や uint64 を保持できません。慣例的な回避策は、カスタムスカラー(BigInt、Long、Int64 などと呼ばれる)を定義し、値を文字列としてシリアライズすることです。本コンバーターはスキーマがそのまま有効になるよう、すべての 64 ビット整数を String として出力します。サーバー側にカスタムスカラーを定義したら、その出現箇所を置き換えてください。
map フィールドはどう扱われますか?
GraphQL にはネイティブな map 型が無く、{ String: String } のような形は存在しません。標準的な回避策は、map を { key, value } ペアのリストに展開することです。たとえば message Order の map<string, string> metadata = 8; は metadata: [OrderMetadataEntry!]! となり、合成された type OrderMetadataEntry { key: String! value: String! } が付きます。entry 型名は親 message + フィールド名の PascalCase + Entry から導出され、これは rejoiner を含むほとんどの proto-to-GraphQL ゲートウェイで使われている慣習です。
oneof はどう扱われますか?
oneof の各フィールドはグループを示すコメント付きの通常の nullable フィールドとして出力されます。GraphQL には proto の oneof にきれいに対応するネイティブな判別共用体の概念がありません — 最も近いのはカスタム union 型か、最近 input 用に仕様に追加された @oneOf ディレクティブです。出力型については、ほとんどのスキーマは oneof の各ケースを nullable として出して制約をドキュメントで補うやり方をしており、ここでも同様です。厳密な union 型が必要なら出力を手で編集してください。
なぜスカラーは非 null で message は nullable なのですか?
これは proto3 のセマンティクスに合わせたものです。proto3 のスカラーはワイヤー上で常に値を持ち、未設定の string は ""、未設定の int32 は 0 になります。optional やラッパー型を使わない限り、スカラーで「既定値に設定済み」と「未設定」を区別する手段はありません。一方で単数の入れ子メッセージには has-value セマンティクスがあり — フィールドタグごと存在しないこともあり得る — 自然に nullable な GraphQL フィールドにマップされます。repeated フィールドは常に非 null 要素の非 null リストで、これも proto3 と一致します。
google.protobuf.Timestamp などの well-known 型はどうなりますか?
GraphQL には組み込みの DateTime スカラーが無いため、well-known 型は既定で String として出力されます。本番のスキーマの多くはカスタム DateTime(あるいは ISO8601String)スカラーを定義し、生成後に String の出現箇所を置き換えています。google.protobuf.Duration、Any、Value も同様 — まず String で出力して、カスタムスカラーを定義したら差し替えます。
bytes フィールド — なぜ String ?
GraphQL にはネイティブなバイナリスカラーがありません。慣例的な符号化は String フィールド内の base64 で、proto3 の JSON マッピングも bytes に対して同じ方式を使っています。より厳密な型付けが欲しいなら、サーバー側に Base64String のカスタムスカラーを定義して、当該フィールドの String を置き換えてください。
関連ツール
Protobuf、GraphQL、JSON を扱っているなら、これらと組み合わせると便利です: