入力(.proto スキーマ)

出力(Python)

このツールでできること

Protocol Buffers スキーマと、それに対応する型を必要とする Python サービスやスクリプトを抱えている、という状況。公式の道筋は Python プラグイン付きの protoc公式 Python チュートリアルを参照)で、生成されたメッセージクラスは動きますが、読みづらく diff でも騒がしい。このツールは代わりに素の dataclass を出力します — クリーンで慣用的、テストでモックしやすい。スキーマを貼り、出力をコピーして、プロジェクトに放り込むだけ。

型マッピングは手で書くであろう内容そのままです。string/bytesstr/bytesboolbool、整数(int32 から sfixed64 まで)はすべて intdouble/floatfloat になります。repeated Tfield(default_factory=list) 付きの list[T]map<K, V>field(default_factory=dict) 付きの dict[K, V]、単数のメッセージ参照は既定値 NoneOptional[Msg] となり、循環参照や前方参照もそのまま動きます。

enum は IntEnum のサブクラスになります。これは公式の Python protobuf ランタイムが内部で使っているもので、ほとんどのレビュアーが見慣れている形です。from __future__ import annotations をファイル先頭に置くことで 遅延評価が前方参照をきれいに処理します — 本体内で型ヒントを文字列で囲む必要はありません。ネストされたメッセージはトップレベルの dataclass にフラット化されます。Python は Java のようにネストの恩恵を受けないし、フラットな名前のほうがインポートが楽です。すべてはブラウザ内で動き、スキーマがページの外に出ることはありません。

使い方

3 ステップ。出力は <code>.py</code> ファイルにそのまま投入できます。

1

.proto スキーマを貼り付ける

左のエディタにスキーマを投入します。先頭の syntax = "proto3"; はあっても良いですが任意です。パーサーはネストした message ブロック、enum 宣言、oneofmap<K, V>、フィールドオプションを扱えます。import は認識はしますがスキップします — 複数ファイルにまたがるスキーマの場合はインポートされる型をインラインで貼り付けてください。

フィールド名はそのまま:.protoorder_id は Python でも order_id のままです。snake_case はすでに Pythonic。クラス名も PascalCase のままで、PEP 8 の慣習に合致します。

2

出力を読む

右側のパネルに、メッセージ 1 つにつき @dataclass が 1 つ、enum 1 つにつき IntEnum サブクラスが 1 つ並びます。enum が先、続いて依存順(子が親より前)にメッセージが並びます。ファイルをプロジェクトに追加し、必要な dataclass をインポートすれば完了です。

3

dataclass を使う

キーワード引数でインスタンスを生成し、普通のオブジェクトのように変更し、dataclasses.asdict()json.dumps で HTTP 転送用にシリアライズします。完全な Protobuf ワイヤー形式のエンコーディングが必要なら、dataclass を protobuf-python に橋渡しするか、gRPC クライアントの前段で型付きシムとして使ってください。

実際に時間を節約できる場面

新しい gRPC Python サービスの型をスケッチする

既存の Protobuf API を消費する新規サービスを始めるところ。まだ protoc は走らせたくないけれど、リクエスト/レスポンス形状向けのきれいな dataclass は欲しい。スキーマを貼り、出力を types.py に放り込み、dataclass に対してビジネスロジックを書き、用意ができたところで後から gRPC Python をつなぎます。

pytest で Protobuf データをモックする

生成された Protobuf メッセージクラスは、各フィールドに専用のセッターがあり、コンストラクタが全フィールドを kwargs として受け取らないため、テストで構築するのが面倒です。手書きの dataclass なら受け取れます — Order(order_id="ORD-42", customer_name="Ava Chen", total_amount=99.50) がそのまま動きます。この出力をフィクスチャやモックとして使い、ワイヤー形式のシリアライズには本物の Protobuf クラスを残してください。

Protobuf API の変更をレビューする

バックエンドの同僚が Order にフィールドを追加し、新しい OrderStatus 値を入れた。フルビルドを走らせずに、Python クライアントコードが何に対応する必要があるかを知りたい。新しい .proto を貼り、dataclass 出力を現行型と diff し、的を絞ったレビューコメントを残します。

クイックスクリプトと一回限りの ETL ジョブ

Protobuf スキーマに沿った JSON ダンプからデータをバックフィルする 50 行のスクリプトを書こうとしている。一回限りのスクリプトのために protoc をセットアップするのはやり過ぎです。ここから dataclass を取って、JSON をそこにパースして、スクリプトを走らせて、捨てる。ビルドステップなし、ツールチェーンなし、リポに残る生成ファイルもなし。

よくある質問

スキーマはどこかに送信されますか?

いいえ。パーサーと Python のエミッターはすべてブラウザ内で JavaScript として動作します。DevTools を開いて、貼り付けながら Network タブを見てください — リクエストはゼロです。スキーマに内部の型名、パッケージパス、サードパーティに送りたくないものが含まれている場合に役立ちます。

なぜ公式の protobuf-python メッセージクラスではなく dataclass なのですか?

protoc から生成されるクラスは動きますが、冗長で、テストでモックしづらく、コードレビューで騒がしい。dataclass なら型付き kwargs、等価比較、きれいな repr が無料で手に入ります。ワイヤー形式のエンコーディングが必要なら、dataclass と公式メッセージ型のあいだを薄いアダプタ層でマッピングできます — 多くのチームは、生成クラスにすべてを型付けするより、こちらの分け方が良いと感じます。

なぜ str 値の enum ではなく IntEnum なのですか?

Protobuf の enum はワイヤーレベルでは整数値です — 各値にタグ番号があります。IntEnum はそれにぴったり一致します:OrderStatus.ORDER_STATUS_PAID は名前付きメンバーであると同時に整数 2 でもあり、JSON でもワイヤー形式でもクリーンに往復します。JSON エンコード用に StrEnum(Python 3.11+)が欲しい場合は、出力中の IntEnum を find-replace してください。

なぜメッセージフィールドが単なる Msg ではなく Optional[Msg] なのですか?

proto3 では、単数のメッセージフィールドは未設定にできます(既定がゼロ値となるスカラと違って、不在に意味があります)。既定値を None にするとそのセマンティクスに合致し、循環参照もコンパイル可能になります — OrderAddress を埋め込み、address が Order を埋め込み返す場合でも、どちらの dataclass も他方より先に定義されている必要はありません。ファイル先頭の from __future__ import annotations によって、PEP 563 経由で前方参照が実行時に解決されます。

map<K, V> はどう扱いますか?

既定で field(default_factory=dict) を持つ dict[K, V] としてレンダリングします。string でないキー(map<int32, string>)の Protobuf マップは dict[int, str] になります。JSON は文字列キーしか持たないので、dict を JSON にシリアライズすると int のキーは文字列になります — これは proto3 JSON 仕様のクセであり、コンバーター側の問題ではありません。

oneof は扱いますか?

oneof の各フィールドは通常の dataclass フィールドとして出力されます。出力は「ちょうど 1 つ」という制約を強制しません — それを実現したいなら Union 型や判別共用体的な構造が必要で、これはランタイムが排他性をどうモデリングするかに依存します。フラットなレイアウトは読みやすく、実際の Python コードベースの大半が採っている形でもあります。より厳密な型付けが必要なら手で編集してください。

関連ツール

Protobuf、JSON、Python を扱っているなら、これらと相性が良いです: