入力(.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 ビット整数をワイヤ上でクォート付き文字列としてエンコードするからです。bytescontentEncoding: "base64" 付きの string になります。google.protobuf.Timestamp のような well-known 型は RFC 3339 に従い format: "date-time" にマッピングされます。変換はすべてブラウザ内で動作し、スキーマがページから外に出ることはありません。

使い方

3 ステップ。出力はバリデーターにそのまま渡せる単一の JSON Schema ドキュメントです。

1

.proto スキーマを貼る

スキーマを左のエディタに落としてください。先頭の syntax = "proto3"; はあっても無くても OK。パーサーはネストされた message ブロック、enum 宣言、oneofmap<K, V>、フィールドオプションを処理します。import ディレクティブは認識されますがスキップされます — 必要ならインポート対象の型をインラインで貼り付けてください。

フィールド名は snake_case のまま保たれます(proto3 JSON エンコーダー がデフォルトで出力するものと同じ — 変換なし)。クライアントが preserve_proto_field_names = false を設定している場合は、プロパティキーを手動で camelCase に変えてください。

2

スキーマを読む

右側に出るのは JSON Schema 2020-12 ドキュメントで、$idtitle、最後に宣言されたルート message を指す最上位の $ref、ファイル内のすべての message と enum を保持する $defs ブロックを含みます。各 message は properties マップを持つオブジェクトスキーマに、各 enum は値名を持つ string スキーマになります。$ref や $defs の意味を再確認したいときは Understanding JSON Schema ガイドを読んでみてください。

3

バリデーターに繋ぎ込む

出力を 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 出力にも常に存在します(""0false[]{} のような空のデフォルト)。そのためスキーマは required 配下に何もリストしません。バリデーション時にフィールドを実際に required にしたい場合は、親 message の required 配列に手動で追加してください。proto3 の optionaloneof も出力では 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-timeduration)が欲しいときは ajv-formats を併せて入れてください。

関連ツール

Protobuf、JSON Schema、バリデーションを扱っているなら、これらの組み合わせが便利です: