Protobuf から JSON Schema への変換ツール
.proto スキーマを貼り付けてください。各 message と enum に対する $defs エントリを 1 つずつ持つ JSON Schema 2020-12 ドキュメントが手に入り、Ajv や任意のモダンなバリデーターですぐ使えます。
入力(.proto スキーマ)
出力(JSON Schema)
このツールでできること
Protocol Buffers スキーマと、JSON を受け取るサービス(webhook ハンドラー、HTTP-transcoded gRPC ゲートウェイ、リクエストがコードに到達する前にバリデーションする API ゲートウェイなど)があるとします。proto をミラーした JSON Schema ドキュメントが欲しい — 入力ペイロードのバリデーション、OpenAPI フラグメントの生成、構造化出力プロンプトへの投入などのために。このコンバーターはそれをブラウザ内で行います — .proto を貼り、JSON Schema をコピーし、バリデーターの設定に投入してください。
出力は JSON Schema draft 2020-12 — 現行のドラフトで、Ajv をはじめあらゆるモダンなバリデーターがサポート済みです。各 message は $defs 配下に type: "object" と properties マップを持つエントリとして配置されます。各 enum は $defs エントリとなり type: "string" と値名のリストが enum に並びます — proto3 が JSON で enum を名前でシリアライズする方式に揃えています。フィールド参照は leaf 名を使った $ref: "#/$defs/MessageName" で解決されるため、ネストされた型も読みやすいまま保たれます。
型マッピングは proto3 JSON マッピング仕様 に従います。string/bool はそのまま対応します。32 ビット int は type: "integer" + format: "int32"、unsigned 32 ビットには minimum: 0 が付きます。64 ビット int は type: "string" + format: "int64" + 数値パターン になります。なぜなら JSON Number は 2^53 を超えると精度を失い、proto3 は 64 ビット整数をワイヤ上でクォート付き文字列としてエンコードするからです。bytes は contentEncoding: "base64" 付きの string になります。google.protobuf.Timestamp のような well-known 型は RFC 3339 に従い format: "date-time" にマッピングされます。変換はすべてブラウザ内で動作し、スキーマがページから外に出ることはありません。
使い方
3 ステップ。出力はバリデーターにそのまま渡せる単一の JSON Schema ドキュメントです。
.proto スキーマを貼る
スキーマを左のエディタに落としてください。先頭の syntax = "proto3"; はあっても無くても OK。パーサーはネストされた message ブロック、enum 宣言、oneof、map<K, V>、フィールドオプションを処理します。import ディレクティブは認識されますがスキップされます — 必要ならインポート対象の型をインラインで貼り付けてください。
フィールド名は snake_case のまま保たれます(proto3 JSON エンコーダー がデフォルトで出力するものと同じ — 変換なし)。クライアントが preserve_proto_field_names = false を設定している場合は、プロパティキーを手動で camelCase に変えてください。
スキーマを読む
右側に出るのは JSON Schema 2020-12 ドキュメントで、$id、title、最後に宣言されたルート message を指す最上位の $ref、ファイル内のすべての message と enum を保持する $defs ブロックを含みます。各 message は properties マップを持つオブジェクトスキーマに、各 enum は値名を持つ string スキーマになります。$ref や $defs の意味を再確認したいときは Understanding JSON Schema ガイドを読んでみてください。
バリデーターに繋ぎ込む
出力を schema.json として保存し、Ajv(Node)、jsonschema(Python)、あるいはお使いのスタックのバリデーターに読み込ませ、gRPC ゲートウェイや webhook が受け取る JSON に対して走らせます。ミスマッチは /items/0/sku must be string のような読みやすいエラーパスで表示されます。同じスキーマは OpenAPI 3.1 のコンポーネント定義や、LLM の構造化出力プロンプトにもそのまま使えます。
これが本当に時間を節約する場面
proto で定義された webhook を検証する
チームでは OrderShipped イベントの真実の源として Protobuf を使っているが、実際の webhook 受信側は JSON を受け取る — 受信側に proto ランタイムは無い。.proto を貼って JSON Schema を Ajv に入れれば、ビジネスロジックに到達する前にエッジで不正なペイロードを弾けます。quantity が欠けた SKU-101 はもうデータベースに辿り着きません。
gRPC スキーマから OpenAPI 3.1 を作る
gRPC ゲートウェイ向けに OpenAPI 3.1 仕様を書いている。OpenAPI 3.1 は JSON Schema 2020-12 互換なので、ここの $defs ブロックは小さなリネームを経て components.schemas 直下にそのまま落とせます。protoc-gen-openapi プラグインのインストールも、Buf CLI のセットアップも不要 — 貼って、編集して、コミットするだけ。
proto から LLM の構造化出力を得る
既存の .proto に合致する型付き Order オブジェクトを OpenAI や Anthropic に返してほしい。スキーマを貼って $defs/Order エントリを取り出し、response_format の JSON Schema として渡します。モデルは手動の coercion なしで gRPC サービスを往復できる出力を生成するようになります。
Protobuf API の変更をレビューする
バックエンドの同僚が Address に 2 つフィールドを追加し、enum 値の名前を変えた。それがゲートウェイで使っている JSON Schema にどう影響するか、フルの codegen パイプラインを回さずに確認したい。新しい .proto を貼って、コミット済みのコピーとスキーマを diff し、PR にピンポイントなレビューコメントを残します。
よくある質問
スキーマはどこかに送信されますか?
いいえ。パーサーと JSON Schema 出力器は完全にブラウザ内で JavaScript として動作します。貼り付けながら DevTools の Network タブを見てみてください — リクエストはゼロです。スキーマに社内 package パス、型名、サードパーティに渡したくないものが含まれているときに役立ちます。
出力はどの JSON Schema ドラフトを対象としますか?
現行の公開ドラフトである Draft 2020-12 です。すべての出力ドキュメントの $schema URI は https://json-schema.org/draft/2020-12/schema です。2019-09 からの変更点は 2020-12 リリースノート を参照してください。アクティブにメンテされているバリデーター(Ajv 8+、Python の jsonschema 4+、NJsonSchema、Java の Justify)はすべてデフォルトで 2020-12 をサポートしています。
なぜ int64 フィールドは integer ではなく string なのですか?
proto3 の JSON マッピング仕様がそう言っているからで、これは正しい判断です。JSON Number は IEEE-754 の double で、2^53 を超えると精度を失います。本物の int64 はその上限をはるかに超える値を運べます — 注文 ID、ナノ秒タイムスタンプ、台帳残高など — そこで proto3 は 64 ビット整数をクォート付きの JSON 文字列としてエンコードします。スキーマは type: "string"、format: "int64"、数値パターンでこれを反映し、バリデーターが "abc" を引き続き拒否できるようにしています。サーバーが 64 ビット int を生の JSON Number として返している(一部のレガシーゲートウェイがそうします)場合は、それらのエントリを手動で { "type": "integer" } に変えてください。
なぜ enum は integer ではなく string なのですか?
同じ理由 — それが proto3 の JSON エンコーディングのデフォルトだからです。enum はワイヤ番号の整数(2)ではなく値名("ORDER_STATUS_PAID")としてシリアライズされます。これにより JSON ペイロードが読みやすくなり、スキーマもシンプルになります。整数番号はワイヤフォーマットの関心事であり、バリデーションの関心事ではないため、JSON Schema には含まれていません。int を出力するように構成された非デフォルトのエンコーダーがある場合は、enum エントリの type: "string" を type: "integer" に置き換えてください。
map<K, V> はどう扱われますか?
{ "type": "object", "additionalProperties": <V-schema> } としてレンダリングされます。JSON のオブジェクトキーは常に文字列なので、proto map で非文字列のキー(例 map<int32, string>)の場合は、ランタイムのキーが文字列に変換されることを説明する記述ノートが付きます。値のスキーマは通常のフィールドと同じ型マッピングルールに従います。
フィールドは required としてマークされますか?
いいえ — proto3 のフィールドはワイヤフォーマットで常にデフォルト値を持ち、JSON 出力にも常に存在します(""、0、false、[]、{} のような空のデフォルト)。そのためスキーマは required 配下に何もリストしません。バリデーション時にフィールドを実際に required にしたい場合は、親 message の required 配列に手動で追加してください。proto3 の optional や oneof も出力では oneOf として強制されません — それらは追加のアノテーション無しに JSON Schema が完全には表現できないランタイムセマンティクスです。
ネストされた message はどのように参照されますか?
すべての message と enum は leaf 名でキー付けされたフラットな $defs ブロックに引き上げられます。フィールド参照は $ref: "#/$defs/MessageName" を経由します。フラット化することでドキュメントがコンパクトに保たれ、二重にネストされた型が重複しません。異なる package の 2 つの message が leaf 名を共有する場合、コンバーターは最初の定義を保持します — それが問題なら貼り付ける前に名前の衝突を解消してください。
これをそのまま Ajv に挿せますか?
はい。Ajv が draft 2020-12 用に構成されていれば(ajv/dist/2020 からの new Ajv2020())、出力に対する ajv.compile(schema) はそのまま動きます。すべてが同じドキュメント内にあるので $ref エントリは内部で解決されます。format バリデーション(date-time、duration)が欲しいときは ajv-formats を併せて入れてください。
関連ツール
Protobuf、JSON Schema、バリデーションを扱っているなら、これらの組み合わせが便利です: