入力(.proto スキーマ)

出力(Rust)

このツールでできること

Protocol Buffers のスキーマがあって、それと通信する Rust のサービスやクライアントを書いている。標準的な手順は build.rs スクリプトに prost(gRPC なら tonic)を組み込むこと — 堅実だが、型を素描してコードと形を突き合わせたいだけのときには大げさだ。このコンバーターはブラウザ内でスキーマを構造体定義に変換する。貼って、コピーして、src/types.rs に落として、作業を続ける。

出力は各構造体に #[derive(Debug, Default, Clone, Serialize, Deserialize)] を付けるので、serde 経由で JSON をラウンドトリップできる。enum は #[repr(i32)] で、ゼロ値のバリアントに #[default] が付くので、Default::default() が proto3 のセマンティクスと一致する。単数形のネストされたメッセージは Option<T> になる — Rust は明示的な nullability を要求し、これは prost が生成する形と一致する。repeated フィールドの Vec<T> と map の HashMap<K, V> はどちらも #[serde(default)] が付き、欠落フィールドはエラーではなく空コンテナにデシリアライズされる。

型のマッピングは proto3 JSON マッピング仕様に従う:string/bytesString/Vec<u8>、各種サイズの整数は i32/i64/u32/u64 に、浮動小数点は f32/f64 に。フィールド名は snake_case に変換される(これはすでに Rust の慣習なので、ほとんどはそのまま通る)。enum バリアントはスクリーミングスネークの enum プレフィックスを落とす — enum OrderStatusORDER_STATUS_UNSPECIFIED は、手で書くなら自然な Unspecified になる。変換は完全にクライアントサイドで動く;スキーマがページから出ることはない。

使い方

3 ステップ。出力は serde を依存に持つどんな Rust クレートでも貼り付けてすぐ使える。

1

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

左のエディタにスキーマを落とす。先頭の syntax = "proto3"; は任意 — パーサーはネストした message ブロック、enum 宣言、oneofmap<K, V>、フィールドオプションを処理する。import は認識するが読み飛ばすので、依存する型は本文に直接貼り付けてほしい。

フィールド名は自動的に Rust の慣習に従う:order_id はそのまま order_id、スキーマに camelCase の shippingAddress があれば shipping_address になる。構造体名と enum 名は PascalCase のまま。

2

出力を読む

右側:pub structpub enum の宣言が平らに並ぶ。enum が先、続いて宣言順の message。ネストした型は親より前。use serde::{Deserialize, Serialize}; の行は常に出る;use std::collections::HashMap; はスキーマに map フィールドがあるときだけ現れる。

3

クレートに投入する

出力をモジュールファイルに貼る。Cargo.tomlserde = { version = "1", features = ["derive"] }serde_json があればコンパイルが通る。あとは普通の Rust だ — reqwest クライアント、Cloudflare Worker、axum ハンドラなど、メッセージデータに型付きでアクセスしたい場所どこにでも繋げばいい。

実際に時間を稼げる場面

新しい Rust サービスの型を素描する

gRPC-gateway のエンドポイントを叩く Rust サービスを作っているが、prost を使った build.rs を本格的に組むのはまだ早い。.proto を貼り付け、構造体を types.rs に落とし、レスポンスを JSON でデコードして、プロトタイプを出す。

Protobuf API の変更レビュー

バックエンドの同僚がメッセージにフィールドを追加した。クレート全体を再ビルドせずに Rust 側の形がどう変わるか見たい。新しい .proto を貼って、既存の型と差分を取り、的を絞ったレビューコメントを残す。

prost が生成したコードと突き合わせる

ビルドで prost をカスタム type_attribute ルール付きで使っていて、改変前の型がどう見えるかきれいなリファレンスが欲しい。ここにスキーマを貼って横並びで確認する。属性変更を計画するときや別の codegen パイプラインへ移行するときに便利だ。

CLI ツールや使い捨てスクリプト

Protobuf 由来の JSON API を叩く小さな Rust の CLI を書いている。100 行のツールのために prost を組み込むのは仕事に対して大げさすぎる。ここから構造体をもらってきて serde を derive し、バイナリを出す。

よくある質問

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

いいえ。パーサーも Rust エミッタもブラウザ内の JavaScript として動く。DevTools を開いて Network タブを見ながら貼ってみてほしい — リクエストはゼロだ。スキーマに内部の型名やパッケージパス、第三者サービスに送りたくないものが含まれているときに役立つ。

なぜネストされたメッセージは Option でラップされるの?

Rust には Java や Go のような nullable 参照がない — 値は存在するか、不在を明示するために Option<T> を使うかのどちらか。proto3 の単数形サブメッセージはワイヤ上で nullable(フィールドタグごと欠落しうる)ので、Rust のイディオムは Address ではなく Option<Address> になる。これは prost が生成する形と一致する。スカラーと enum はラップされない — ゼロ値(空文字列、0、false、#[default] の enum バリアント)が有効な Rust の値だからだ。

なぜ 64 ビット整数は string ではなく i64/u64 で型付けされるの?

Rust では精度の問題はない — i64u64 はファーストクラスの型で、64 ビット値が string になる JavaScript とは違う。厳密な proto3 JSON エンコーダが出力した JSON をデシリアライズするなら、値は string で届く;serde は自動的には変換しない。その場合は対象フィールドに #[serde(with = "serde_with::DisplayFromStr")] やカスタムデシリアライザを付けてほしい。

なぜ BTreeMap ではなく HashMap?

HashMap は prost が map<K, V> フィールドに出すものと一致するし、デフォルトとして妥当 — proto の map フィールドは順序を持たない。安定したイテレーション順が必要なら、型を手で BTreeMap に変えればいい;他のコードを変える必要はない。

この出力を prost や tonic と一緒に使うべき?

これは serde で derive した素の構造体で、prost でエンコード済みのものではない。serde_json 経由で proto3 JSON のラウンドトリップはするが、バイナリのワイヤ形式はエンコード/デコードできない。バイナリの protobuf エンコードが必要なら build.rs ステップで prost を回す。gRPC が必要なら tonic を使う。このコンバーターは JSON-over-HTTP のケース(gRPC-gateway、Connect、JSON トランスコーディング)向けだ。

oneof は扱える?

oneof の各フィールドは普通の構造体フィールドとして出る。出力は oneof が暗示する「ちょうど 1 つ」の制約を強制しない — そのためにはケースごとにバリアントを持つ Rust の enum が必要で、それはランタイムのセマンティクス次第になる。厳密な oneof 処理が必要なら、出力を手で pub enum に書き換えてほしい。

関連ツール

Protobuf、JSON、Rust を扱っているなら、これらは相性がいい: