How To Generate an OpenAPI Spec With gRPC Gateway
You may want to provide a RESTful API in addition to your gRPC service without the need to duplicate your code.
gRPC Gateway (opens in a new tab) is a popular tool for generating RESTful APIs from gRPC service definitions.
In this tutorial, we'll take a detailed look at how to use gRPC Gateway to generate an OpenAPI schema based on a Protocol Buffers (protobuf) gRPC service definition. Afterward, we can use Speakeasy to read our generated OpenAPI schema and create a production-ready SDK.
TIP
An Overview of gRPC Gateway
gRPC Gateway (opens in a new tab) is a protoc (opens in a new tab) plugin that reads gRPC service definitions and generates a reverse proxy server that translates a RESTful JSON API into gRPC.
This way, you can expose an HTTP endpoint that can be called by clients that don't support gRPC. The generated server code will forward incoming JSON requests to your gRPC server and translate the responses to JSON.
gRPC Gateway also generates an OpenAPI schema that describes your API. You can use this schema to create SDKs for your API.
OpenAPI Versions
gRPC Gateway outputs OpenAPI 2.0, and Speakeasy supports OpenAPI 3.0 and 3.1. To generate an OpenAPI 3.0 or 3.1 schema, you'll need to convert the OpenAPI 2.0 schema to at least OpenAPI 3.0.
The Protobuf to REST SDK Pipeline
To generate a REST API with a developer-friendly SDK, we'll follow these three core steps:
-
gRPC to OpenAPI: First, we will use gRPC Gateway to produce an OpenAPI schema based on our protobuf service definition. This generated schema is in OpenAPI 2.0 format.
-
OpenAPI 2.0 to OpenAPI 3.x: Next, as gRPC Gateway's output schema is in OpenAPI 2.0 and we need at least OpenAPI 3.0 for our SDK, we will convert the generated schema from OpenAPI 2.0 to OpenAPI 3.0.
-
OpenAPI 3.x to SDK: Finally, once we have the OpenAPI 3.0 schema, we will leverage Speakeasy to create our SDK based on the OpenAPI 3.0 schema derived from the previous steps.
By following these steps, we can ensure we have a robust, production-ready SDK that adheres to our API's specifications.
Step-by-Step Tutorial: From Protobuf to OpenAPI to an SDK
Now let's walk through generating an OpenAPI schema and SDK for our Speakeasy Bar gRPC service.
Check Out the Example Repository
If you would like to follow along, start by cloning the example repository:
git clone git@github.com:speakeasy-api/speakeasy-grpc-gateway-example.gitcd speakeasy-grpc-gateway-example
Install Go
To generate an OpenAPI schema from a protobuf file, we'll need to install Go and protoc.
This tutorial was written using Go 1.21.4.
On macOS, install Go by running:
brew install go
Alternatively, follow the Go installation instructions (opens in a new tab) for your platform.
Install Buf
We'll use the Buf CLI (opens in a new tab) as an alternative to protoc so that we can save our generation configuration as YAML. Buf is compatible with protoc plugins.
On macOS, install Buf by running:
brew install bufbuild/buf/buf
Alternatively, follow the Buf CLI installation instructions (opens in a new tab) for your platform.
Install Buf Modules
We'll use Buf modules to manage our dependencies.
cd protobuf mod updatecd ..
Install protoc-gen-go
Buf requires the protoc-gen-go
plugin to generate Go code from protobuf files.
Install protoc-gen-go
by running:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
Make sure that the protoc-gen-go
binary is in your $PATH
. On macOS, you can achieve that by running the following command if the go/bin
directory is not already in your path.
export PATH=${PATH}:`go env GOPATH`/bin
Install Go Requirements
go mod tidygo install \ github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway \ github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2 \ google.golang.org/protobuf/cmd/protoc-gen-go \ google.golang.org/grpc/cmd/protoc-gen-go-grpc
Generate the Go Code
We'll use Buf to generate the Go code from the protobuf file.
Run the following in the terminal:
buf generate
Buf reads the configuration in buf.gen.yaml
, then generates the Go code in the proto
directory.
This will generate the proto/speakeasy/v1/speakeasy.pb.go
, proto/speakeasy/v1/speakeasy_grpc.pb.go
, and proto/speakeasy/v1/speakeasy.pb.gw.go
files.
Generate the OpenAPI Schema
Because we have the openapiv2
protoc plugin configured in our buf.gen.yaml
file, Buf will generate an OpenAPI schema and save it as openapi/speakeasy/v1/speakeasy.swagger.json
.
This is the OpenAPI 2.0 schema that gRPC Gateway generates by default.
Convert the OpenAPI Schema to OpenAPI 3.0
We'll use the excellent kin-openapi (opens in a new tab) Go library to convert the OpenAPI 2.0 schema to OpenAPI 3.0.
In convert/convert.go
, we use kin-openapi
to unmarshal openapi/speakeasy/v1/speakeasy.swagger.json
, then convert it to OpenAPI 3.0, then marshal it back to JSON, and finally write it to openapi/speakeasy/v1/speakeasy.openapi.json
.
To do the conversion, run the following in the terminal:
go run convert/convert.go
How To Customize the API Schema
By modifying the protobuf service definition, we can customize the generated OpenAPI schema.
We'll start with a basic example and add options to enhance the schema.
Meet the Speakeasy Bar Protobuf Service
We'll start by taking a look at the Speakeasy Bar protobuf service definition in proto/speakeasy/v1/speakeasy.proto
.
syntax = "proto3";package speakeasy;import "google/protobuf/empty.proto";import "google/api/field_behavior.proto";import "google/api/annotations.proto";message Drink { string name = 1 [ (google.api.field_behavior) = REQUIRED ]; enum DrinkType { DRINK_TYPE_UNSPECIFIED = 0; DRINK_TYPE_WINE = 1; DRINK_TYPE_COCKTAIL = 2; DRINK_TYPE_MOCKTAIL = 3; DRINK_TYPE_SOFT = 4; DRINK_TYPE_SPIRIT = 5; DRINK_TYPE_OTHER = 6; DRINK_TYPE_BEER = 7; }; DrinkType type = 2 [ (google.api.field_behavior) = REQUIRED ]; double price = 3; int32 stock = 4; string productCode = 5;};message ListDrinksResponse { repeated Drink drinks = 1; }message ListDrinksRequest { google.protobuf.Empty empty = 1; }message GetDrinkResponse { Drink drink = 1; }message GetDrinkRequest { string product_code = 1; }service SpeakeasyService { // List all drinks rpc ListDrinks(ListDrinksRequest) returns (ListDrinksResponse) { option (google.api.http) = { get: "/v1/drinks" response_body: "drinks" }; }; // Get a drink by product code rpc GetDrink(GetDrinkRequest) returns (GetDrinkResponse) { option (google.api.http) = { get: "/v1/drinks/{product_code}" response_body: "drink" }; };};
The service defines one object type, called Drink
.
syntax = "proto3";package speakeasy;import "google/protobuf/empty.proto";import "google/api/field_behavior.proto";import "google/api/annotations.proto";message Drink { string name = 1 [ (google.api.field_behavior) = REQUIRED ]; enum DrinkType { DRINK_TYPE_UNSPECIFIED = 0; DRINK_TYPE_WINE = 1; DRINK_TYPE_COCKTAIL = 2; DRINK_TYPE_MOCKTAIL = 3; DRINK_TYPE_SOFT = 4; DRINK_TYPE_SPIRIT = 5; DRINK_TYPE_OTHER = 6; DRINK_TYPE_BEER = 7; }; DrinkType type = 2 [ (google.api.field_behavior) = REQUIRED ]; double price = 3; int32 stock = 4; string productCode = 5;};message ListDrinksResponse { repeated Drink drinks = 1; }message ListDrinksRequest { google.protobuf.Empty empty = 1; }message GetDrinkResponse { Drink drink = 1; }message GetDrinkRequest { string product_code = 1; }service SpeakeasyService { // List all drinks rpc ListDrinks(ListDrinksRequest) returns (ListDrinksResponse) { option (google.api.http) = { get: "/v1/drinks" response_body: "drinks" }; }; // Get a drink by product code rpc GetDrink(GetDrinkRequest) returns (GetDrinkResponse) { option (google.api.http) = { get: "/v1/drinks/{product_code}" response_body: "drink" }; };};
A service called SpeakeasyService
has two methods, GetDrink
and ListDrinks
.
syntax = "proto3";package speakeasy;import "google/protobuf/empty.proto";import "google/api/field_behavior.proto";import "google/api/annotations.proto";message Drink { string name = 1 [ (google.api.field_behavior) = REQUIRED ]; enum DrinkType { DRINK_TYPE_UNSPECIFIED = 0; DRINK_TYPE_WINE = 1; DRINK_TYPE_COCKTAIL = 2; DRINK_TYPE_MOCKTAIL = 3; DRINK_TYPE_SOFT = 4; DRINK_TYPE_SPIRIT = 5; DRINK_TYPE_OTHER = 6; DRINK_TYPE_BEER = 7; }; DrinkType type = 2 [ (google.api.field_behavior) = REQUIRED ]; double price = 3; int32 stock = 4; string productCode = 5;};message ListDrinksResponse { repeated Drink drinks = 1; }message ListDrinksRequest { google.protobuf.Empty empty = 1; }message GetDrinkResponse { Drink drink = 1; }message GetDrinkRequest { string product_code = 1; }service SpeakeasyService { // List all drinks rpc ListDrinks(ListDrinksRequest) returns (ListDrinksResponse) { option (google.api.http) = { get: "/v1/drinks" response_body: "drinks" }; }; // Get a drink by product code rpc GetDrink(GetDrinkRequest) returns (GetDrinkResponse) { option (google.api.http) = { get: "/v1/drinks/{product_code}" response_body: "drink" }; };};
Add API Information to the Service
We'll add information about the API to the service definition using options.openapiv2_swagger
from grpc.gateway.protoc_gen_openapiv2
.
syntax = "proto3";package speakeasy;import "google/protobuf/empty.proto";import "google/api/field_behavior.proto";import "google/api/annotations.proto";import "protoc-gen-openapiv2/options/annotations.proto";option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { info: { version: "1.0", title: "Speakeasy API", description: "Speakeasy API description" }; host: "127.0.0.1:8080"; external_docs: { url: "https://github.com/speakeasy-api/speakeasy-grpc-gateway-example"; description: "Speakeasy API documentation"; }; schemes: HTTPS; tags: { name: "drinks"; description: "Drinks API"; };};message Drink { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { json_schema: { title: "A drink", description: "A drink served at the speakeasy" } example: "{\"name\": \"Gin and Tonic\",\"type\": \"DRINK_TYPE_COCKTAIL\",\"price\": 5.99,\"stock\": 10,\"productCode\": \"2438ac3c-37eb-4902-adef-ed16b4431030\"}"; }; string name = 1 [ (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The name of the drink", example: "\"Gin and Tonic\"" } ]; enum DrinkType { DRINK_TYPE_UNSPECIFIED = 0; DRINK_TYPE_WINE = 1; DRINK_TYPE_COCKTAIL = 2; DRINK_TYPE_MOCKTAIL = 3; DRINK_TYPE_SOFT = 4; DRINK_TYPE_SPIRIT = 5; DRINK_TYPE_OTHER = 6; DRINK_TYPE_BEER = 7; }; DrinkType type = 2 [ (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The type of drink", example: "\"DRINK_TYPE_COCKTAIL\"" } ]; double price = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The price of the drink", minimum: 0, example: "5.99" } ]; int32 stock = 4 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The stock of the drink", minimum: 0, example: "10" } ]; string productCode = 5 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$", description: "Unique drink identifier for server requests", format: "uuid", example: "\"2438ac3c-37eb-4902-adef-ed16b4431030\"" }];};message ListDrinksResponse { repeated Drink drinks = 1; }message ListDrinksRequest { google.protobuf.Empty empty = 1; }message GetDrinkResponse { Drink drink = 1; }message GetDrinkRequest { string product_code = 1 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$", description: "Unique drink identifier", format: "uuid", example: "\"2438ac3c-37eb-4902-adef-ed16b4431030\"" }]; }service SpeakeasyService { // List all drinks rpc ListDrinks(ListDrinksRequest) returns (ListDrinksResponse) { option (google.api.http) = { get: "/v1/drinks" response_body: "drinks" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Get a list of drinks"; description: "Returns a list of all drinks available at the speakeasy"; operation_id: "listAllDrinks"; tags: "drinks"; }; }; // Get a drink by product code rpc GetDrink(GetDrinkRequest) returns (GetDrinkResponse) { option (google.api.http) = { get: "/v1/drinks/{product_code}" response_body: "drink" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Get a drink"; operation_id: "getDrink"; tags: "drinks"; extensions: { key: "x-speakeasy-retries"; value: { struct_value: { fields { key: "strategy"; value { string_value: "backoff"; } } fields { key: "backoff"; value { struct_value: { fields { key: "initialInterval"; value { number_value: 500; } } fields { key: "maxInterval"; value { number_value: 60000; } } fields { key: "maxElapsedTime"; value { number_value: 3600000; } } fields { key: "exponent"; value { number_value: 1.5; } } } } } fields { key: "statusCodes"; value { list_value: { values { string_value: "5XX"; } } } } fields { key: "retryConnectionErrors"; value { bool_value: true; } } } } } }; };};
We'll add a title, description, and version to the API.
This appears in the info
object in the generated OpenAPI schema.
syntax = "proto3";package speakeasy;import "google/protobuf/empty.proto";import "google/api/field_behavior.proto";import "google/api/annotations.proto";import "protoc-gen-openapiv2/options/annotations.proto";option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { info: { version: "1.0", title: "Speakeasy API", description: "Speakeasy API description" }; host: "127.0.0.1:8080"; external_docs: { url: "https://github.com/speakeasy-api/speakeasy-grpc-gateway-example"; description: "Speakeasy API documentation"; }; schemes: HTTPS; tags: { name: "drinks"; description: "Drinks API"; };};message Drink { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { json_schema: { title: "A drink", description: "A drink served at the speakeasy" } example: "{\"name\": \"Gin and Tonic\",\"type\": \"DRINK_TYPE_COCKTAIL\",\"price\": 5.99,\"stock\": 10,\"productCode\": \"2438ac3c-37eb-4902-adef-ed16b4431030\"}"; }; string name = 1 [ (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The name of the drink", example: "\"Gin and Tonic\"" } ]; enum DrinkType { DRINK_TYPE_UNSPECIFIED = 0; DRINK_TYPE_WINE = 1; DRINK_TYPE_COCKTAIL = 2; DRINK_TYPE_MOCKTAIL = 3; DRINK_TYPE_SOFT = 4; DRINK_TYPE_SPIRIT = 5; DRINK_TYPE_OTHER = 6; DRINK_TYPE_BEER = 7; }; DrinkType type = 2 [ (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The type of drink", example: "\"DRINK_TYPE_COCKTAIL\"" } ]; double price = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The price of the drink", minimum: 0, example: "5.99" } ]; int32 stock = 4 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The stock of the drink", minimum: 0, example: "10" } ]; string productCode = 5 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$", description: "Unique drink identifier for server requests", format: "uuid", example: "\"2438ac3c-37eb-4902-adef-ed16b4431030\"" }];};message ListDrinksResponse { repeated Drink drinks = 1; }message ListDrinksRequest { google.protobuf.Empty empty = 1; }message GetDrinkResponse { Drink drink = 1; }message GetDrinkRequest { string product_code = 1 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$", description: "Unique drink identifier", format: "uuid", example: "\"2438ac3c-37eb-4902-adef-ed16b4431030\"" }]; }service SpeakeasyService { // List all drinks rpc ListDrinks(ListDrinksRequest) returns (ListDrinksResponse) { option (google.api.http) = { get: "/v1/drinks" response_body: "drinks" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Get a list of drinks"; description: "Returns a list of all drinks available at the speakeasy"; operation_id: "listAllDrinks"; tags: "drinks"; }; }; // Get a drink by product code rpc GetDrink(GetDrinkRequest) returns (GetDrinkResponse) { option (google.api.http) = { get: "/v1/drinks/{product_code}" response_body: "drink" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Get a drink"; operation_id: "getDrink"; tags: "drinks"; extensions: { key: "x-speakeasy-retries"; value: { struct_value: { fields { key: "strategy"; value { string_value: "backoff"; } } fields { key: "backoff"; value { struct_value: { fields { key: "initialInterval"; value { number_value: 500; } } fields { key: "maxInterval"; value { number_value: 60000; } } fields { key: "maxElapsedTime"; value { number_value: 3600000; } } fields { key: "exponent"; value { number_value: 1.5; } } } } } fields { key: "statusCodes"; value { list_value: { values { string_value: "5XX"; } } } } fields { key: "retryConnectionErrors"; value { bool_value: true; } } } } } }; };};
{ "components": { "schemas": { "DrinkDrinkType": { "default": "DRINK_TYPE_UNSPECIFIED", "enum": [ "DRINK_TYPE_UNSPECIFIED", "DRINK_TYPE_WINE", "DRINK_TYPE_COCKTAIL", "DRINK_TYPE_MOCKTAIL", "DRINK_TYPE_SOFT", "DRINK_TYPE_SPIRIT", "DRINK_TYPE_OTHER", "DRINK_TYPE_BEER" ], "type": "string" }, "protobufAny": { "additionalProperties": {}, "properties": { "@type": { "type": "string" } }, "type": "object" }, "rpcStatus": { "properties": { "code": { "format": "int32", "type": "integer" }, "details": { "items": { "$ref": "#/components/schemas/protobufAny" }, "type": "array" }, "message": { "type": "string" } }, "type": "object" }, "speakeasyDrink": { "description": "A drink served at the speakeasy", "example": { "name": "Gin and Tonic", "price": 5.99, "productCode": "2438ac3c-37eb-4902-adef-ed16b4431030", "stock": 10, "type": "DRINK_TYPE_COCKTAIL" }, "properties": { "name": { "description": "The name of the drink", "example": "Gin and Tonic", "type": "string" }, "price": { "description": "The price of the drink", "example": 5.99, "format": "double", "type": "number" }, "productCode": { "description": "Unique drink identifier for server requests", "example": "2438ac3c-37eb-4902-adef-ed16b4431030", "format": "uuid", "pattern": "^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$", "type": "string" }, "stock": { "description": "The stock of the drink", "example": 10, "format": "int32", "type": "integer" }, "type": { "$ref": "#/components/schemas/DrinkDrinkType" } }, "required": ["name", "type"], "title": "A drink", "type": "object" }, "speakeasyGetDrinkResponse": { "properties": { "drink": { "$ref": "#/components/schemas/speakeasyDrink" } }, "type": "object" }, "speakeasyListDrinksResponse": { "properties": { "drinks": { "items": { "$ref": "#/components/schemas/speakeasyDrink" }, "type": "array" } }, "type": "object" } } }, "externalDocs": { "description": "Speakeasy API documentation", "url": "https://github.com/speakeasy-api/speakeasy-grpc-gateway-example" }, "info": { "description": "Speakeasy API description", "title": "Speakeasy API", "version": "1.0" }, "openapi": "3.0.3", "paths": { "/v1/drinks": { "get": { "description": "Returns a list of all drinks available at the speakeasy", "operationId": "listAllDrinks", "parameters": [ { "in": "query", "name": "empty", "schema": { "type": "object" } } ], "responses": { "200": { "content": { "application/json": { "schema": { "items": { "$ref": "#/components/schemas/speakeasyDrink" }, "type": "array" } } }, "description": "" }, "default": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/rpcStatus" } } }, "description": "An unexpected error response." } }, "summary": "Get a list of drinks", "tags": ["drinks"] } }, "/v1/drinks/{productCode}": { "get": { "operationId": "getDrink", "parameters": [ { "description": "Unique drink identifier", "in": "path", "name": "productCode", "required": true, "schema": { "format": "uuid", "type": "string" } } ], "responses": { "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/speakeasyDrink" } } }, "description": "" }, "default": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/rpcStatus" } } }, "description": "An unexpected error response." } }, "summary": "Get a drink", "tags": ["drinks"], "x-speakeasy-retries": { "backoff": { "exponent": 1.5, "initialInterval": 500, "maxElapsedTime": 3600000, "maxInterval": 60000 }, "retryConnectionErrors": true, "statusCodes": ["5XX"], "strategy": "backoff" } } } }, "servers": [ { "url": "https://127.0.0.1:8080/" } ], "tags": [ { "description": "Drinks API", "name": "drinks" }, { "name": "SpeakeasyService" } ]}
We'll add a server to the API using the host
key.
Our conversion script will add the servers
object to the generated OpenAPI schema.
syntax = "proto3";package speakeasy;import "google/protobuf/empty.proto";import "google/api/field_behavior.proto";import "google/api/annotations.proto";import "protoc-gen-openapiv2/options/annotations.proto";option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { info: { version: "1.0", title: "Speakeasy API", description: "Speakeasy API description" }; host: "127.0.0.1:8080"; external_docs: { url: "https://github.com/speakeasy-api/speakeasy-grpc-gateway-example"; description: "Speakeasy API documentation"; }; schemes: HTTPS; tags: { name: "drinks"; description: "Drinks API"; };};message Drink { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { json_schema: { title: "A drink", description: "A drink served at the speakeasy" } example: "{\"name\": \"Gin and Tonic\",\"type\": \"DRINK_TYPE_COCKTAIL\",\"price\": 5.99,\"stock\": 10,\"productCode\": \"2438ac3c-37eb-4902-adef-ed16b4431030\"}"; }; string name = 1 [ (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The name of the drink", example: "\"Gin and Tonic\"" } ]; enum DrinkType { DRINK_TYPE_UNSPECIFIED = 0; DRINK_TYPE_WINE = 1; DRINK_TYPE_COCKTAIL = 2; DRINK_TYPE_MOCKTAIL = 3; DRINK_TYPE_SOFT = 4; DRINK_TYPE_SPIRIT = 5; DRINK_TYPE_OTHER = 6; DRINK_TYPE_BEER = 7; }; DrinkType type = 2 [ (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The type of drink", example: "\"DRINK_TYPE_COCKTAIL\"" } ]; double price = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The price of the drink", minimum: 0, example: "5.99" } ]; int32 stock = 4 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The stock of the drink", minimum: 0, example: "10" } ]; string productCode = 5 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$", description: "Unique drink identifier for server requests", format: "uuid", example: "\"2438ac3c-37eb-4902-adef-ed16b4431030\"" }];};message ListDrinksResponse { repeated Drink drinks = 1; }message ListDrinksRequest { google.protobuf.Empty empty = 1; }message GetDrinkResponse { Drink drink = 1; }message GetDrinkRequest { string product_code = 1 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$", description: "Unique drink identifier", format: "uuid", example: "\"2438ac3c-37eb-4902-adef-ed16b4431030\"" }]; }service SpeakeasyService { // List all drinks rpc ListDrinks(ListDrinksRequest) returns (ListDrinksResponse) { option (google.api.http) = { get: "/v1/drinks" response_body: "drinks" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Get a list of drinks"; description: "Returns a list of all drinks available at the speakeasy"; operation_id: "listAllDrinks"; tags: "drinks"; }; }; // Get a drink by product code rpc GetDrink(GetDrinkRequest) returns (GetDrinkResponse) { option (google.api.http) = { get: "/v1/drinks/{product_code}" response_body: "drink" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Get a drink"; operation_id: "getDrink"; tags: "drinks"; extensions: { key: "x-speakeasy-retries"; value: { struct_value: { fields { key: "strategy"; value { string_value: "backoff"; } } fields { key: "backoff"; value { struct_value: { fields { key: "initialInterval"; value { number_value: 500; } } fields { key: "maxInterval"; value { number_value: 60000; } } fields { key: "maxElapsedTime"; value { number_value: 3600000; } } fields { key: "exponent"; value { number_value: 1.5; } } } } } fields { key: "statusCodes"; value { list_value: { values { string_value: "5XX"; } } } } fields { key: "retryConnectionErrors"; value { bool_value: true; } } } } } }; };};
{ "components": { "schemas": { "DrinkDrinkType": { "default": "DRINK_TYPE_UNSPECIFIED", "enum": [ "DRINK_TYPE_UNSPECIFIED", "DRINK_TYPE_WINE", "DRINK_TYPE_COCKTAIL", "DRINK_TYPE_MOCKTAIL", "DRINK_TYPE_SOFT", "DRINK_TYPE_SPIRIT", "DRINK_TYPE_OTHER", "DRINK_TYPE_BEER" ], "type": "string" }, "protobufAny": { "additionalProperties": {}, "properties": { "@type": { "type": "string" } }, "type": "object" }, "rpcStatus": { "properties": { "code": { "format": "int32", "type": "integer" }, "details": { "items": { "$ref": "#/components/schemas/protobufAny" }, "type": "array" }, "message": { "type": "string" } }, "type": "object" }, "speakeasyDrink": { "description": "A drink served at the speakeasy", "example": { "name": "Gin and Tonic", "price": 5.99, "productCode": "2438ac3c-37eb-4902-adef-ed16b4431030", "stock": 10, "type": "DRINK_TYPE_COCKTAIL" }, "properties": { "name": { "description": "The name of the drink", "example": "Gin and Tonic", "type": "string" }, "price": { "description": "The price of the drink", "example": 5.99, "format": "double", "type": "number" }, "productCode": { "description": "Unique drink identifier for server requests", "example": "2438ac3c-37eb-4902-adef-ed16b4431030", "format": "uuid", "pattern": "^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$", "type": "string" }, "stock": { "description": "The stock of the drink", "example": 10, "format": "int32", "type": "integer" }, "type": { "$ref": "#/components/schemas/DrinkDrinkType" } }, "required": ["name", "type"], "title": "A drink", "type": "object" }, "speakeasyGetDrinkResponse": { "properties": { "drink": { "$ref": "#/components/schemas/speakeasyDrink" } }, "type": "object" }, "speakeasyListDrinksResponse": { "properties": { "drinks": { "items": { "$ref": "#/components/schemas/speakeasyDrink" }, "type": "array" } }, "type": "object" } } }, "externalDocs": { "description": "Speakeasy API documentation", "url": "https://github.com/speakeasy-api/speakeasy-grpc-gateway-example" }, "info": { "description": "Speakeasy API description", "title": "Speakeasy API", "version": "1.0" }, "openapi": "3.0.3", "paths": { "/v1/drinks": { "get": { "description": "Returns a list of all drinks available at the speakeasy", "operationId": "listAllDrinks", "parameters": [ { "in": "query", "name": "empty", "schema": { "type": "object" } } ], "responses": { "200": { "content": { "application/json": { "schema": { "items": { "$ref": "#/components/schemas/speakeasyDrink" }, "type": "array" } } }, "description": "" }, "default": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/rpcStatus" } } }, "description": "An unexpected error response." } }, "summary": "Get a list of drinks", "tags": ["drinks"] } }, "/v1/drinks/{productCode}": { "get": { "operationId": "getDrink", "parameters": [ { "description": "Unique drink identifier", "in": "path", "name": "productCode", "required": true, "schema": { "format": "uuid", "type": "string" } } ], "responses": { "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/speakeasyDrink" } } }, "description": "" }, "default": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/rpcStatus" } } }, "description": "An unexpected error response." } }, "summary": "Get a drink", "tags": ["drinks"], "x-speakeasy-retries": { "backoff": { "exponent": 1.5, "initialInterval": 500, "maxElapsedTime": 3600000, "maxInterval": 60000 }, "retryConnectionErrors": true, "statusCodes": ["5XX"], "strategy": "backoff" } } } }, "servers": [ { "url": "https://127.0.0.1:8080/" } ], "tags": [ { "description": "Drinks API", "name": "drinks" }, { "name": "SpeakeasyService" } ]}
Add Descriptions and Examples to Components
To create an SDK that offers a great developer experience, we recommend adding descriptions and examples to all fields in OpenAPI components.
We'll start with the Drink
object type.
syntax = "proto3";package speakeasy;import "google/protobuf/empty.proto";import "google/api/field_behavior.proto";import "google/api/annotations.proto";import "protoc-gen-openapiv2/options/annotations.proto";option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { info: { version: "1.0", title: "Speakeasy API", description: "Speakeasy API description" }; host: "127.0.0.1:8080"; external_docs: { url: "https://github.com/speakeasy-api/speakeasy-grpc-gateway-example"; description: "Speakeasy API documentation"; }; schemes: HTTPS; tags: { name: "drinks"; description: "Drinks API"; };};message Drink { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { json_schema: { title: "A drink", description: "A drink served at the speakeasy" } example: "{\"name\": \"Gin and Tonic\",\"type\": \"DRINK_TYPE_COCKTAIL\",\"price\": 5.99,\"stock\": 10,\"productCode\": \"2438ac3c-37eb-4902-adef-ed16b4431030\"}"; }; string name = 1 [ (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The name of the drink", example: "\"Gin and Tonic\"" } ]; enum DrinkType { DRINK_TYPE_UNSPECIFIED = 0; DRINK_TYPE_WINE = 1; DRINK_TYPE_COCKTAIL = 2; DRINK_TYPE_MOCKTAIL = 3; DRINK_TYPE_SOFT = 4; DRINK_TYPE_SPIRIT = 5; DRINK_TYPE_OTHER = 6; DRINK_TYPE_BEER = 7; }; DrinkType type = 2 [ (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The type of drink", example: "\"DRINK_TYPE_COCKTAIL\"" } ]; double price = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The price of the drink", minimum: 0, example: "5.99" } ]; int32 stock = 4 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The stock of the drink", minimum: 0, example: "10" } ]; string productCode = 5 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$", description: "Unique drink identifier for server requests", format: "uuid", example: "\"2438ac3c-37eb-4902-adef-ed16b4431030\"" }];};message ListDrinksResponse { repeated Drink drinks = 1; }message ListDrinksRequest { google.protobuf.Empty empty = 1; }message GetDrinkResponse { Drink drink = 1; }message GetDrinkRequest { string product_code = 1 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$", description: "Unique drink identifier", format: "uuid", example: "\"2438ac3c-37eb-4902-adef-ed16b4431030\"" }]; }service SpeakeasyService { // List all drinks rpc ListDrinks(ListDrinksRequest) returns (ListDrinksResponse) { option (google.api.http) = { get: "/v1/drinks" response_body: "drinks" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Get a list of drinks"; description: "Returns a list of all drinks available at the speakeasy"; operation_id: "listAllDrinks"; tags: "drinks"; }; }; // Get a drink by product code rpc GetDrink(GetDrinkRequest) returns (GetDrinkResponse) { option (google.api.http) = { get: "/v1/drinks/{product_code}" response_body: "drink" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Get a drink"; operation_id: "getDrink"; tags: "drinks"; extensions: { key: "x-speakeasy-retries"; value: { struct_value: { fields { key: "strategy"; value { string_value: "backoff"; } } fields { key: "backoff"; value { struct_value: { fields { key: "initialInterval"; value { number_value: 500; } } fields { key: "maxInterval"; value { number_value: 60000; } } fields { key: "maxElapsedTime"; value { number_value: 3600000; } } fields { key: "exponent"; value { number_value: 1.5; } } } } } fields { key: "statusCodes"; value { list_value: { values { string_value: "5XX"; } } } } fields { key: "retryConnectionErrors"; value { bool_value: true; } } } } } }; };};
We added a title
, description
, and example
to the Drink
object type.
Note that the example
is a stringified JSON object.
syntax = "proto3";package speakeasy;import "google/protobuf/empty.proto";import "google/api/field_behavior.proto";import "google/api/annotations.proto";import "protoc-gen-openapiv2/options/annotations.proto";option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { info: { version: "1.0", title: "Speakeasy API", description: "Speakeasy API description" }; host: "127.0.0.1:8080"; external_docs: { url: "https://github.com/speakeasy-api/speakeasy-grpc-gateway-example"; description: "Speakeasy API documentation"; }; schemes: HTTPS; tags: { name: "drinks"; description: "Drinks API"; };};message Drink { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { json_schema: { title: "A drink", description: "A drink served at the speakeasy" } example: "{\"name\": \"Gin and Tonic\",\"type\": \"DRINK_TYPE_COCKTAIL\",\"price\": 5.99,\"stock\": 10,\"productCode\": \"2438ac3c-37eb-4902-adef-ed16b4431030\"}"; }; string name = 1 [ (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The name of the drink", example: "\"Gin and Tonic\"" } ]; enum DrinkType { DRINK_TYPE_UNSPECIFIED = 0; DRINK_TYPE_WINE = 1; DRINK_TYPE_COCKTAIL = 2; DRINK_TYPE_MOCKTAIL = 3; DRINK_TYPE_SOFT = 4; DRINK_TYPE_SPIRIT = 5; DRINK_TYPE_OTHER = 6; DRINK_TYPE_BEER = 7; }; DrinkType type = 2 [ (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The type of drink", example: "\"DRINK_TYPE_COCKTAIL\"" } ]; double price = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The price of the drink", minimum: 0, example: "5.99" } ]; int32 stock = 4 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The stock of the drink", minimum: 0, example: "10" } ]; string productCode = 5 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$", description: "Unique drink identifier for server requests", format: "uuid", example: "\"2438ac3c-37eb-4902-adef-ed16b4431030\"" }];};message ListDrinksResponse { repeated Drink drinks = 1; }message ListDrinksRequest { google.protobuf.Empty empty = 1; }message GetDrinkResponse { Drink drink = 1; }message GetDrinkRequest { string product_code = 1 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$", description: "Unique drink identifier", format: "uuid", example: "\"2438ac3c-37eb-4902-adef-ed16b4431030\"" }]; }service SpeakeasyService { // List all drinks rpc ListDrinks(ListDrinksRequest) returns (ListDrinksResponse) { option (google.api.http) = { get: "/v1/drinks" response_body: "drinks" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Get a list of drinks"; description: "Returns a list of all drinks available at the speakeasy"; operation_id: "listAllDrinks"; tags: "drinks"; }; }; // Get a drink by product code rpc GetDrink(GetDrinkRequest) returns (GetDrinkResponse) { option (google.api.http) = { get: "/v1/drinks/{product_code}" response_body: "drink" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Get a drink"; operation_id: "getDrink"; tags: "drinks"; extensions: { key: "x-speakeasy-retries"; value: { struct_value: { fields { key: "strategy"; value { string_value: "backoff"; } } fields { key: "backoff"; value { struct_value: { fields { key: "initialInterval"; value { number_value: 500; } } fields { key: "maxInterval"; value { number_value: 60000; } } fields { key: "maxElapsedTime"; value { number_value: 3600000; } } fields { key: "exponent"; value { number_value: 1.5; } } } } } fields { key: "statusCodes"; value { list_value: { values { string_value: "5XX"; } } } } fields { key: "retryConnectionErrors"; value { bool_value: true; } } } } } }; };};
We use openapiv2_field
to add options to the fields in the Drink
object type.
For example, we added a description
, pattern
, format, and example
to the productCode
field.
syntax = "proto3";package speakeasy;import "google/protobuf/empty.proto";import "google/api/field_behavior.proto";import "google/api/annotations.proto";import "protoc-gen-openapiv2/options/annotations.proto";option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { info: { version: "1.0", title: "Speakeasy API", description: "Speakeasy API description" }; host: "127.0.0.1:8080"; external_docs: { url: "https://github.com/speakeasy-api/speakeasy-grpc-gateway-example"; description: "Speakeasy API documentation"; }; schemes: HTTPS; tags: { name: "drinks"; description: "Drinks API"; };};message Drink { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { json_schema: { title: "A drink", description: "A drink served at the speakeasy" } example: "{\"name\": \"Gin and Tonic\",\"type\": \"DRINK_TYPE_COCKTAIL\",\"price\": 5.99,\"stock\": 10,\"productCode\": \"2438ac3c-37eb-4902-adef-ed16b4431030\"}"; }; string name = 1 [ (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The name of the drink", example: "\"Gin and Tonic\"" } ]; enum DrinkType { DRINK_TYPE_UNSPECIFIED = 0; DRINK_TYPE_WINE = 1; DRINK_TYPE_COCKTAIL = 2; DRINK_TYPE_MOCKTAIL = 3; DRINK_TYPE_SOFT = 4; DRINK_TYPE_SPIRIT = 5; DRINK_TYPE_OTHER = 6; DRINK_TYPE_BEER = 7; }; DrinkType type = 2 [ (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The type of drink", example: "\"DRINK_TYPE_COCKTAIL\"" } ]; double price = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The price of the drink", minimum: 0, example: "5.99" } ]; int32 stock = 4 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The stock of the drink", minimum: 0, example: "10" } ]; string productCode = 5 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$", description: "Unique drink identifier for server requests", format: "uuid", example: "\"2438ac3c-37eb-4902-adef-ed16b4431030\"" }];};message ListDrinksResponse { repeated Drink drinks = 1; }message ListDrinksRequest { google.protobuf.Empty empty = 1; }message GetDrinkResponse { Drink drink = 1; }message GetDrinkRequest { string product_code = 1 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$", description: "Unique drink identifier", format: "uuid", example: "\"2438ac3c-37eb-4902-adef-ed16b4431030\"" }]; }service SpeakeasyService { // List all drinks rpc ListDrinks(ListDrinksRequest) returns (ListDrinksResponse) { option (google.api.http) = { get: "/v1/drinks" response_body: "drinks" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Get a list of drinks"; description: "Returns a list of all drinks available at the speakeasy"; operation_id: "listAllDrinks"; tags: "drinks"; }; }; // Get a drink by product code rpc GetDrink(GetDrinkRequest) returns (GetDrinkResponse) { option (google.api.http) = { get: "/v1/drinks/{product_code}" response_body: "drink" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Get a drink"; operation_id: "getDrink"; tags: "drinks"; extensions: { key: "x-speakeasy-retries"; value: { struct_value: { fields { key: "strategy"; value { string_value: "backoff"; } } fields { key: "backoff"; value { struct_value: { fields { key: "initialInterval"; value { number_value: 500; } } fields { key: "maxInterval"; value { number_value: 60000; } } fields { key: "maxElapsedTime"; value { number_value: 3600000; } } fields { key: "exponent"; value { number_value: 1.5; } } } } } fields { key: "statusCodes"; value { list_value: { values { string_value: "5XX"; } } } } fields { key: "retryConnectionErrors"; value { bool_value: true; } } } } } }; };};
{ "components": { "schemas": { "DrinkDrinkType": { "default": "DRINK_TYPE_UNSPECIFIED", "enum": [ "DRINK_TYPE_UNSPECIFIED", "DRINK_TYPE_WINE", "DRINK_TYPE_COCKTAIL", "DRINK_TYPE_MOCKTAIL", "DRINK_TYPE_SOFT", "DRINK_TYPE_SPIRIT", "DRINK_TYPE_OTHER", "DRINK_TYPE_BEER" ], "type": "string" }, "protobufAny": { "additionalProperties": {}, "properties": { "@type": { "type": "string" } }, "type": "object" }, "rpcStatus": { "properties": { "code": { "format": "int32", "type": "integer" }, "details": { "items": { "$ref": "#/components/schemas/protobufAny" }, "type": "array" }, "message": { "type": "string" } }, "type": "object" }, "speakeasyDrink": { "description": "A drink served at the speakeasy", "example": { "name": "Gin and Tonic", "price": 5.99, "productCode": "2438ac3c-37eb-4902-adef-ed16b4431030", "stock": 10, "type": "DRINK_TYPE_COCKTAIL" }, "properties": { "name": { "description": "The name of the drink", "example": "Gin and Tonic", "type": "string" }, "price": { "description": "The price of the drink", "example": 5.99, "format": "double", "type": "number" }, "productCode": { "description": "Unique drink identifier for server requests", "example": "2438ac3c-37eb-4902-adef-ed16b4431030", "format": "uuid", "pattern": "^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$", "type": "string" }, "stock": { "description": "The stock of the drink", "example": 10, "format": "int32", "type": "integer" }, "type": { "$ref": "#/components/schemas/DrinkDrinkType" } }, "required": ["name", "type"], "title": "A drink", "type": "object" }, "speakeasyGetDrinkResponse": { "properties": { "drink": { "$ref": "#/components/schemas/speakeasyDrink" } }, "type": "object" }, "speakeasyListDrinksResponse": { "properties": { "drinks": { "items": { "$ref": "#/components/schemas/speakeasyDrink" }, "type": "array" } }, "type": "object" } } }, "externalDocs": { "description": "Speakeasy API documentation", "url": "https://github.com/speakeasy-api/speakeasy-grpc-gateway-example" }, "info": { "description": "Speakeasy API description", "title": "Speakeasy API", "version": "1.0" }, "openapi": "3.0.3", "paths": { "/v1/drinks": { "get": { "description": "Returns a list of all drinks available at the speakeasy", "operationId": "listAllDrinks", "parameters": [ { "in": "query", "name": "empty", "schema": { "type": "object" } } ], "responses": { "200": { "content": { "application/json": { "schema": { "items": { "$ref": "#/components/schemas/speakeasyDrink" }, "type": "array" } } }, "description": "" }, "default": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/rpcStatus" } } }, "description": "An unexpected error response." } }, "summary": "Get a list of drinks", "tags": ["drinks"] } }, "/v1/drinks/{productCode}": { "get": { "operationId": "getDrink", "parameters": [ { "description": "Unique drink identifier", "in": "path", "name": "productCode", "required": true, "schema": { "format": "uuid", "type": "string" } } ], "responses": { "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/speakeasyDrink" } } }, "description": "" }, "default": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/rpcStatus" } } }, "description": "An unexpected error response." } }, "summary": "Get a drink", "tags": ["drinks"], "x-speakeasy-retries": { "backoff": { "exponent": 1.5, "initialInterval": 500, "maxElapsedTime": 3600000, "maxInterval": 60000 }, "retryConnectionErrors": true, "statusCodes": ["5XX"], "strategy": "backoff" } } } }, "servers": [ { "url": "https://127.0.0.1:8080/" } ], "tags": [ { "description": "Drinks API", "name": "drinks" }, { "name": "SpeakeasyService" } ]}
If you use Speakeasy to create an SDK, this description and example will appear in the generated documentation and usage examples.
This usage example is from the TypeScript SDK's documentation.
Note how the productCode
field is represented by our UUID example instead of a random string.
import { SDK } from "openapi";(async() => { const sdk = new SDK(); const res = await sdk.drinks.getDrink({ productCode: "602a7da9-b8bb-46e6-b288-457b561029b8", }); if (res.statusCode == 200) { // handle response }})();
Customize the OperationId
By default, the operationId
is the method name in the protobuf service definition.
We can customize the operationId
for each method using options.openapiv2_operation
.
syntax = "proto3";package speakeasy;import "google/protobuf/empty.proto";import "google/api/field_behavior.proto";import "google/api/annotations.proto";import "protoc-gen-openapiv2/options/annotations.proto";option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { info: { version: "1.0", title: "Speakeasy API", description: "Speakeasy API description" }; host: "127.0.0.1:8080"; external_docs: { url: "https://github.com/speakeasy-api/speakeasy-grpc-gateway-example"; description: "Speakeasy API documentation"; }; schemes: HTTPS; tags: { name: "drinks"; description: "Drinks API"; };};message Drink { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { json_schema: { title: "A drink", description: "A drink served at the speakeasy" } example: "{\"name\": \"Gin and Tonic\",\"type\": \"DRINK_TYPE_COCKTAIL\",\"price\": 5.99,\"stock\": 10,\"productCode\": \"2438ac3c-37eb-4902-adef-ed16b4431030\"}"; }; string name = 1 [ (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The name of the drink", example: "\"Gin and Tonic\"" } ]; enum DrinkType { DRINK_TYPE_UNSPECIFIED = 0; DRINK_TYPE_WINE = 1; DRINK_TYPE_COCKTAIL = 2; DRINK_TYPE_MOCKTAIL = 3; DRINK_TYPE_SOFT = 4; DRINK_TYPE_SPIRIT = 5; DRINK_TYPE_OTHER = 6; DRINK_TYPE_BEER = 7; }; DrinkType type = 2 [ (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The type of drink", example: "\"DRINK_TYPE_COCKTAIL\"" } ]; double price = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The price of the drink", minimum: 0, example: "5.99" } ]; int32 stock = 4 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The stock of the drink", minimum: 0, example: "10" } ]; string productCode = 5 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$", description: "Unique drink identifier for server requests", format: "uuid", example: "\"2438ac3c-37eb-4902-adef-ed16b4431030\"" }];};message ListDrinksResponse { repeated Drink drinks = 1; }message ListDrinksRequest { google.protobuf.Empty empty = 1; }message GetDrinkResponse { Drink drink = 1; }message GetDrinkRequest { string product_code = 1 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$", description: "Unique drink identifier", format: "uuid", example: "\"2438ac3c-37eb-4902-adef-ed16b4431030\"" }]; }service SpeakeasyService { // List all drinks rpc ListDrinks(ListDrinksRequest) returns (ListDrinksResponse) { option (google.api.http) = { get: "/v1/drinks" response_body: "drinks" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Get a list of drinks"; description: "Returns a list of all drinks available at the speakeasy"; operation_id: "listAllDrinks"; tags: "drinks"; }; }; // Get a drink by product code rpc GetDrink(GetDrinkRequest) returns (GetDrinkResponse) { option (google.api.http) = { get: "/v1/drinks/{product_code}" response_body: "drink" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Get a drink"; operation_id: "getDrink"; tags: "drinks"; extensions: { key: "x-speakeasy-retries"; value: { struct_value: { fields { key: "strategy"; value { string_value: "backoff"; } } fields { key: "backoff"; value { struct_value: { fields { key: "initialInterval"; value { number_value: 500; } } fields { key: "maxInterval"; value { number_value: 60000; } } fields { key: "maxElapsedTime"; value { number_value: 3600000; } } fields { key: "exponent"; value { number_value: 1.5; } } } } } fields { key: "statusCodes"; value { list_value: { values { string_value: "5XX"; } } } } fields { key: "retryConnectionErrors"; value { bool_value: true; } } } } } }; };};
{ "components": { "schemas": { "DrinkDrinkType": { "default": "DRINK_TYPE_UNSPECIFIED", "enum": [ "DRINK_TYPE_UNSPECIFIED", "DRINK_TYPE_WINE", "DRINK_TYPE_COCKTAIL", "DRINK_TYPE_MOCKTAIL", "DRINK_TYPE_SOFT", "DRINK_TYPE_SPIRIT", "DRINK_TYPE_OTHER", "DRINK_TYPE_BEER" ], "type": "string" }, "protobufAny": { "additionalProperties": {}, "properties": { "@type": { "type": "string" } }, "type": "object" }, "rpcStatus": { "properties": { "code": { "format": "int32", "type": "integer" }, "details": { "items": { "$ref": "#/components/schemas/protobufAny" }, "type": "array" }, "message": { "type": "string" } }, "type": "object" }, "speakeasyDrink": { "description": "A drink served at the speakeasy", "example": { "name": "Gin and Tonic", "price": 5.99, "productCode": "2438ac3c-37eb-4902-adef-ed16b4431030", "stock": 10, "type": "DRINK_TYPE_COCKTAIL" }, "properties": { "name": { "description": "The name of the drink", "example": "Gin and Tonic", "type": "string" }, "price": { "description": "The price of the drink", "example": 5.99, "format": "double", "type": "number" }, "productCode": { "description": "Unique drink identifier for server requests", "example": "2438ac3c-37eb-4902-adef-ed16b4431030", "format": "uuid", "pattern": "^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$", "type": "string" }, "stock": { "description": "The stock of the drink", "example": 10, "format": "int32", "type": "integer" }, "type": { "$ref": "#/components/schemas/DrinkDrinkType" } }, "required": ["name", "type"], "title": "A drink", "type": "object" }, "speakeasyGetDrinkResponse": { "properties": { "drink": { "$ref": "#/components/schemas/speakeasyDrink" } }, "type": "object" }, "speakeasyListDrinksResponse": { "properties": { "drinks": { "items": { "$ref": "#/components/schemas/speakeasyDrink" }, "type": "array" } }, "type": "object" } } }, "externalDocs": { "description": "Speakeasy API documentation", "url": "https://github.com/speakeasy-api/speakeasy-grpc-gateway-example" }, "info": { "description": "Speakeasy API description", "title": "Speakeasy API", "version": "1.0" }, "openapi": "3.0.3", "paths": { "/v1/drinks": { "get": { "description": "Returns a list of all drinks available at the speakeasy", "operationId": "listAllDrinks", "parameters": [ { "in": "query", "name": "empty", "schema": { "type": "object" } } ], "responses": { "200": { "content": { "application/json": { "schema": { "items": { "$ref": "#/components/schemas/speakeasyDrink" }, "type": "array" } } }, "description": "" }, "default": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/rpcStatus" } } }, "description": "An unexpected error response." } }, "summary": "Get a list of drinks", "tags": ["drinks"] } }, "/v1/drinks/{productCode}": { "get": { "operationId": "getDrink", "parameters": [ { "description": "Unique drink identifier", "in": "path", "name": "productCode", "required": true, "schema": { "format": "uuid", "type": "string" } } ], "responses": { "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/speakeasyDrink" } } }, "description": "" }, "default": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/rpcStatus" } } }, "description": "An unexpected error response." } }, "summary": "Get a drink", "tags": ["drinks"], "x-speakeasy-retries": { "backoff": { "exponent": 1.5, "initialInterval": 500, "maxElapsedTime": 3600000, "maxInterval": 60000 }, "retryConnectionErrors": true, "statusCodes": ["5XX"], "strategy": "backoff" } } } }, "servers": [ { "url": "https://127.0.0.1:8080/" } ], "tags": [ { "description": "Drinks API", "name": "drinks" }, { "name": "SpeakeasyService" } ]}
Add Descriptions and Tags to Methods
We can add descriptions and tags to methods using options.openapiv2_operation
.
syntax = "proto3";package speakeasy;import "google/protobuf/empty.proto";import "google/api/field_behavior.proto";import "google/api/annotations.proto";import "protoc-gen-openapiv2/options/annotations.proto";option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { info: { version: "1.0", title: "Speakeasy API", description: "Speakeasy API description" }; host: "127.0.0.1:8080"; external_docs: { url: "https://github.com/speakeasy-api/speakeasy-grpc-gateway-example"; description: "Speakeasy API documentation"; }; schemes: HTTPS; tags: { name: "drinks"; description: "Drinks API"; };};message Drink { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { json_schema: { title: "A drink", description: "A drink served at the speakeasy" } example: "{\"name\": \"Gin and Tonic\",\"type\": \"DRINK_TYPE_COCKTAIL\",\"price\": 5.99,\"stock\": 10,\"productCode\": \"2438ac3c-37eb-4902-adef-ed16b4431030\"}"; }; string name = 1 [ (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The name of the drink", example: "\"Gin and Tonic\"" } ]; enum DrinkType { DRINK_TYPE_UNSPECIFIED = 0; DRINK_TYPE_WINE = 1; DRINK_TYPE_COCKTAIL = 2; DRINK_TYPE_MOCKTAIL = 3; DRINK_TYPE_SOFT = 4; DRINK_TYPE_SPIRIT = 5; DRINK_TYPE_OTHER = 6; DRINK_TYPE_BEER = 7; }; DrinkType type = 2 [ (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The type of drink", example: "\"DRINK_TYPE_COCKTAIL\"" } ]; double price = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The price of the drink", minimum: 0, example: "5.99" } ]; int32 stock = 4 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The stock of the drink", minimum: 0, example: "10" } ]; string productCode = 5 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$", description: "Unique drink identifier for server requests", format: "uuid", example: "\"2438ac3c-37eb-4902-adef-ed16b4431030\"" }];};message ListDrinksResponse { repeated Drink drinks = 1; }message ListDrinksRequest { google.protobuf.Empty empty = 1; }message GetDrinkResponse { Drink drink = 1; }message GetDrinkRequest { string product_code = 1 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$", description: "Unique drink identifier", format: "uuid", example: "\"2438ac3c-37eb-4902-adef-ed16b4431030\"" }]; }service SpeakeasyService { // List all drinks rpc ListDrinks(ListDrinksRequest) returns (ListDrinksResponse) { option (google.api.http) = { get: "/v1/drinks" response_body: "drinks" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Get a list of drinks"; description: "Returns a list of all drinks available at the speakeasy"; operation_id: "listAllDrinks"; tags: "drinks"; }; }; // Get a drink by product code rpc GetDrink(GetDrinkRequest) returns (GetDrinkResponse) { option (google.api.http) = { get: "/v1/drinks/{product_code}" response_body: "drink" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Get a drink"; operation_id: "getDrink"; tags: "drinks"; extensions: { key: "x-speakeasy-retries"; value: { struct_value: { fields { key: "strategy"; value { string_value: "backoff"; } } fields { key: "backoff"; value { struct_value: { fields { key: "initialInterval"; value { number_value: 500; } } fields { key: "maxInterval"; value { number_value: 60000; } } fields { key: "maxElapsedTime"; value { number_value: 3600000; } } fields { key: "exponent"; value { number_value: 1.5; } } } } } fields { key: "statusCodes"; value { list_value: { values { string_value: "5XX"; } } } } fields { key: "retryConnectionErrors"; value { bool_value: true; } } } } } }; };};
{ "components": { "schemas": { "DrinkDrinkType": { "default": "DRINK_TYPE_UNSPECIFIED", "enum": [ "DRINK_TYPE_UNSPECIFIED", "DRINK_TYPE_WINE", "DRINK_TYPE_COCKTAIL", "DRINK_TYPE_MOCKTAIL", "DRINK_TYPE_SOFT", "DRINK_TYPE_SPIRIT", "DRINK_TYPE_OTHER", "DRINK_TYPE_BEER" ], "type": "string" }, "protobufAny": { "additionalProperties": {}, "properties": { "@type": { "type": "string" } }, "type": "object" }, "rpcStatus": { "properties": { "code": { "format": "int32", "type": "integer" }, "details": { "items": { "$ref": "#/components/schemas/protobufAny" }, "type": "array" }, "message": { "type": "string" } }, "type": "object" }, "speakeasyDrink": { "description": "A drink served at the speakeasy", "example": { "name": "Gin and Tonic", "price": 5.99, "productCode": "2438ac3c-37eb-4902-adef-ed16b4431030", "stock": 10, "type": "DRINK_TYPE_COCKTAIL" }, "properties": { "name": { "description": "The name of the drink", "example": "Gin and Tonic", "type": "string" }, "price": { "description": "The price of the drink", "example": 5.99, "format": "double", "type": "number" }, "productCode": { "description": "Unique drink identifier for server requests", "example": "2438ac3c-37eb-4902-adef-ed16b4431030", "format": "uuid", "pattern": "^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$", "type": "string" }, "stock": { "description": "The stock of the drink", "example": 10, "format": "int32", "type": "integer" }, "type": { "$ref": "#/components/schemas/DrinkDrinkType" } }, "required": ["name", "type"], "title": "A drink", "type": "object" }, "speakeasyGetDrinkResponse": { "properties": { "drink": { "$ref": "#/components/schemas/speakeasyDrink" } }, "type": "object" }, "speakeasyListDrinksResponse": { "properties": { "drinks": { "items": { "$ref": "#/components/schemas/speakeasyDrink" }, "type": "array" } }, "type": "object" } } }, "externalDocs": { "description": "Speakeasy API documentation", "url": "https://github.com/speakeasy-api/speakeasy-grpc-gateway-example" }, "info": { "description": "Speakeasy API description", "title": "Speakeasy API", "version": "1.0" }, "openapi": "3.0.3", "paths": { "/v1/drinks": { "get": { "description": "Returns a list of all drinks available at the speakeasy", "operationId": "listAllDrinks", "parameters": [ { "in": "query", "name": "empty", "schema": { "type": "object" } } ], "responses": { "200": { "content": { "application/json": { "schema": { "items": { "$ref": "#/components/schemas/speakeasyDrink" }, "type": "array" } } }, "description": "" }, "default": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/rpcStatus" } } }, "description": "An unexpected error response." } }, "summary": "Get a list of drinks", "tags": ["drinks"] } }, "/v1/drinks/{productCode}": { "get": { "operationId": "getDrink", "parameters": [ { "description": "Unique drink identifier", "in": "path", "name": "productCode", "required": true, "schema": { "format": "uuid", "type": "string" } } ], "responses": { "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/speakeasyDrink" } } }, "description": "" }, "default": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/rpcStatus" } } }, "description": "An unexpected error response." } }, "summary": "Get a drink", "tags": ["drinks"], "x-speakeasy-retries": { "backoff": { "exponent": 1.5, "initialInterval": 500, "maxElapsedTime": 3600000, "maxInterval": 60000 }, "retryConnectionErrors": true, "statusCodes": ["5XX"], "strategy": "backoff" } } } }, "servers": [ { "url": "https://127.0.0.1:8080/" } ], "tags": [ { "description": "Drinks API", "name": "drinks" }, { "name": "SpeakeasyService" } ]}
Add Tag Descriptions
We can add descriptions to tags in the protobuf definition by using options.openapiv2_swagger
.
In the code example, we added a description to the drinks
tag.
syntax = "proto3";package speakeasy;import "google/protobuf/empty.proto";import "google/api/field_behavior.proto";import "google/api/annotations.proto";import "protoc-gen-openapiv2/options/annotations.proto";option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { info: { version: "1.0", title: "Speakeasy API", description: "Speakeasy API description" }; host: "127.0.0.1:8080"; external_docs: { url: "https://github.com/speakeasy-api/speakeasy-grpc-gateway-example"; description: "Speakeasy API documentation"; }; schemes: HTTPS; tags: { name: "drinks"; description: "Drinks API"; };};message Drink { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { json_schema: { title: "A drink", description: "A drink served at the speakeasy" } example: "{\"name\": \"Gin and Tonic\",\"type\": \"DRINK_TYPE_COCKTAIL\",\"price\": 5.99,\"stock\": 10,\"productCode\": \"2438ac3c-37eb-4902-adef-ed16b4431030\"}"; }; string name = 1 [ (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The name of the drink", example: "\"Gin and Tonic\"" } ]; enum DrinkType { DRINK_TYPE_UNSPECIFIED = 0; DRINK_TYPE_WINE = 1; DRINK_TYPE_COCKTAIL = 2; DRINK_TYPE_MOCKTAIL = 3; DRINK_TYPE_SOFT = 4; DRINK_TYPE_SPIRIT = 5; DRINK_TYPE_OTHER = 6; DRINK_TYPE_BEER = 7; }; DrinkType type = 2 [ (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The type of drink", example: "\"DRINK_TYPE_COCKTAIL\"" } ]; double price = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The price of the drink", minimum: 0, example: "5.99" } ]; int32 stock = 4 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The stock of the drink", minimum: 0, example: "10" } ]; string productCode = 5 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$", description: "Unique drink identifier for server requests", format: "uuid", example: "\"2438ac3c-37eb-4902-adef-ed16b4431030\"" }];};message ListDrinksResponse { repeated Drink drinks = 1; }message ListDrinksRequest { google.protobuf.Empty empty = 1; }message GetDrinkResponse { Drink drink = 1; }message GetDrinkRequest { string product_code = 1 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$", description: "Unique drink identifier", format: "uuid", example: "\"2438ac3c-37eb-4902-adef-ed16b4431030\"" }]; }service SpeakeasyService { // List all drinks rpc ListDrinks(ListDrinksRequest) returns (ListDrinksResponse) { option (google.api.http) = { get: "/v1/drinks" response_body: "drinks" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Get a list of drinks"; description: "Returns a list of all drinks available at the speakeasy"; operation_id: "listAllDrinks"; tags: "drinks"; }; }; // Get a drink by product code rpc GetDrink(GetDrinkRequest) returns (GetDrinkResponse) { option (google.api.http) = { get: "/v1/drinks/{product_code}" response_body: "drink" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Get a drink"; operation_id: "getDrink"; tags: "drinks"; extensions: { key: "x-speakeasy-retries"; value: { struct_value: { fields { key: "strategy"; value { string_value: "backoff"; } } fields { key: "backoff"; value { struct_value: { fields { key: "initialInterval"; value { number_value: 500; } } fields { key: "maxInterval"; value { number_value: 60000; } } fields { key: "maxElapsedTime"; value { number_value: 3600000; } } fields { key: "exponent"; value { number_value: 1.5; } } } } } fields { key: "statusCodes"; value { list_value: { values { string_value: "5XX"; } } } } fields { key: "retryConnectionErrors"; value { bool_value: true; } } } } } }; };};
{ "components": { "schemas": { "DrinkDrinkType": { "default": "DRINK_TYPE_UNSPECIFIED", "enum": [ "DRINK_TYPE_UNSPECIFIED", "DRINK_TYPE_WINE", "DRINK_TYPE_COCKTAIL", "DRINK_TYPE_MOCKTAIL", "DRINK_TYPE_SOFT", "DRINK_TYPE_SPIRIT", "DRINK_TYPE_OTHER", "DRINK_TYPE_BEER" ], "type": "string" }, "protobufAny": { "additionalProperties": {}, "properties": { "@type": { "type": "string" } }, "type": "object" }, "rpcStatus": { "properties": { "code": { "format": "int32", "type": "integer" }, "details": { "items": { "$ref": "#/components/schemas/protobufAny" }, "type": "array" }, "message": { "type": "string" } }, "type": "object" }, "speakeasyDrink": { "description": "A drink served at the speakeasy", "example": { "name": "Gin and Tonic", "price": 5.99, "productCode": "2438ac3c-37eb-4902-adef-ed16b4431030", "stock": 10, "type": "DRINK_TYPE_COCKTAIL" }, "properties": { "name": { "description": "The name of the drink", "example": "Gin and Tonic", "type": "string" }, "price": { "description": "The price of the drink", "example": 5.99, "format": "double", "type": "number" }, "productCode": { "description": "Unique drink identifier for server requests", "example": "2438ac3c-37eb-4902-adef-ed16b4431030", "format": "uuid", "pattern": "^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$", "type": "string" }, "stock": { "description": "The stock of the drink", "example": 10, "format": "int32", "type": "integer" }, "type": { "$ref": "#/components/schemas/DrinkDrinkType" } }, "required": ["name", "type"], "title": "A drink", "type": "object" }, "speakeasyGetDrinkResponse": { "properties": { "drink": { "$ref": "#/components/schemas/speakeasyDrink" } }, "type": "object" }, "speakeasyListDrinksResponse": { "properties": { "drinks": { "items": { "$ref": "#/components/schemas/speakeasyDrink" }, "type": "array" } }, "type": "object" } } }, "externalDocs": { "description": "Speakeasy API documentation", "url": "https://github.com/speakeasy-api/speakeasy-grpc-gateway-example" }, "info": { "description": "Speakeasy API description", "title": "Speakeasy API", "version": "1.0" }, "openapi": "3.0.3", "paths": { "/v1/drinks": { "get": { "description": "Returns a list of all drinks available at the speakeasy", "operationId": "listAllDrinks", "parameters": [ { "in": "query", "name": "empty", "schema": { "type": "object" } } ], "responses": { "200": { "content": { "application/json": { "schema": { "items": { "$ref": "#/components/schemas/speakeasyDrink" }, "type": "array" } } }, "description": "" }, "default": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/rpcStatus" } } }, "description": "An unexpected error response." } }, "summary": "Get a list of drinks", "tags": ["drinks"] } }, "/v1/drinks/{productCode}": { "get": { "operationId": "getDrink", "parameters": [ { "description": "Unique drink identifier", "in": "path", "name": "productCode", "required": true, "schema": { "format": "uuid", "type": "string" } } ], "responses": { "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/speakeasyDrink" } } }, "description": "" }, "default": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/rpcStatus" } } }, "description": "An unexpected error response." } }, "summary": "Get a drink", "tags": ["drinks"], "x-speakeasy-retries": { "backoff": { "exponent": 1.5, "initialInterval": 500, "maxElapsedTime": 3600000, "maxInterval": 60000 }, "retryConnectionErrors": true, "statusCodes": ["5XX"], "strategy": "backoff" } } } }, "servers": [ { "url": "https://127.0.0.1:8080/" } ], "tags": [ { "description": "Drinks API", "name": "drinks" }, { "name": "SpeakeasyService" } ]}
Add OpenAPI Extensions
gRPC Gateway allows us to add OpenAPI extensions to the OpenAPI schema using the extensions
key in our protobuf service definition.
For example, we can add the Speakeasy retries extension x-speakeasy-retries
, which will cause the SDK to retry failed requests.
In the code example, we added the x-speakeasy-retries
extension to the GetDrink
method.
syntax = "proto3";package speakeasy;import "google/protobuf/empty.proto";import "google/api/field_behavior.proto";import "google/api/annotations.proto";import "protoc-gen-openapiv2/options/annotations.proto";option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { info: { version: "1.0", title: "Speakeasy API", description: "Speakeasy API description" }; host: "127.0.0.1:8080"; external_docs: { url: "https://github.com/speakeasy-api/speakeasy-grpc-gateway-example"; description: "Speakeasy API documentation"; }; schemes: HTTPS; tags: { name: "drinks"; description: "Drinks API"; };};message Drink { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { json_schema: { title: "A drink", description: "A drink served at the speakeasy" } example: "{\"name\": \"Gin and Tonic\",\"type\": \"DRINK_TYPE_COCKTAIL\",\"price\": 5.99,\"stock\": 10,\"productCode\": \"2438ac3c-37eb-4902-adef-ed16b4431030\"}"; }; string name = 1 [ (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The name of the drink", example: "\"Gin and Tonic\"" } ]; enum DrinkType { DRINK_TYPE_UNSPECIFIED = 0; DRINK_TYPE_WINE = 1; DRINK_TYPE_COCKTAIL = 2; DRINK_TYPE_MOCKTAIL = 3; DRINK_TYPE_SOFT = 4; DRINK_TYPE_SPIRIT = 5; DRINK_TYPE_OTHER = 6; DRINK_TYPE_BEER = 7; }; DrinkType type = 2 [ (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The type of drink", example: "\"DRINK_TYPE_COCKTAIL\"" } ]; double price = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The price of the drink", minimum: 0, example: "5.99" } ]; int32 stock = 4 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The stock of the drink", minimum: 0, example: "10" } ]; string productCode = 5 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$", description: "Unique drink identifier for server requests", format: "uuid", example: "\"2438ac3c-37eb-4902-adef-ed16b4431030\"" }];};message ListDrinksResponse { repeated Drink drinks = 1; }message ListDrinksRequest { google.protobuf.Empty empty = 1; }message GetDrinkResponse { Drink drink = 1; }message GetDrinkRequest { string product_code = 1 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$", description: "Unique drink identifier", format: "uuid", example: "\"2438ac3c-37eb-4902-adef-ed16b4431030\"" }]; }service SpeakeasyService { // List all drinks rpc ListDrinks(ListDrinksRequest) returns (ListDrinksResponse) { option (google.api.http) = { get: "/v1/drinks" response_body: "drinks" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Get a list of drinks"; description: "Returns a list of all drinks available at the speakeasy"; operation_id: "listAllDrinks"; tags: "drinks"; }; }; // Get a drink by product code rpc GetDrink(GetDrinkRequest) returns (GetDrinkResponse) { option (google.api.http) = { get: "/v1/drinks/{product_code}" response_body: "drink" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Get a drink"; operation_id: "getDrink"; tags: "drinks"; extensions: { key: "x-speakeasy-retries"; value: { struct_value: { fields { key: "strategy"; value { string_value: "backoff"; } } fields { key: "backoff"; value { struct_value: { fields { key: "initialInterval"; value { number_value: 500; } } fields { key: "maxInterval"; value { number_value: 60000; } } fields { key: "maxElapsedTime"; value { number_value: 3600000; } } fields { key: "exponent"; value { number_value: 1.5; } } } } } fields { key: "statusCodes"; value { list_value: { values { string_value: "5XX"; } } } } fields { key: "retryConnectionErrors"; value { bool_value: true; } } } } } }; };};
{ "components": { "schemas": { "DrinkDrinkType": { "default": "DRINK_TYPE_UNSPECIFIED", "enum": [ "DRINK_TYPE_UNSPECIFIED", "DRINK_TYPE_WINE", "DRINK_TYPE_COCKTAIL", "DRINK_TYPE_MOCKTAIL", "DRINK_TYPE_SOFT", "DRINK_TYPE_SPIRIT", "DRINK_TYPE_OTHER", "DRINK_TYPE_BEER" ], "type": "string" }, "protobufAny": { "additionalProperties": {}, "properties": { "@type": { "type": "string" } }, "type": "object" }, "rpcStatus": { "properties": { "code": { "format": "int32", "type": "integer" }, "details": { "items": { "$ref": "#/components/schemas/protobufAny" }, "type": "array" }, "message": { "type": "string" } }, "type": "object" }, "speakeasyDrink": { "description": "A drink served at the speakeasy", "example": { "name": "Gin and Tonic", "price": 5.99, "productCode": "2438ac3c-37eb-4902-adef-ed16b4431030", "stock": 10, "type": "DRINK_TYPE_COCKTAIL" }, "properties": { "name": { "description": "The name of the drink", "example": "Gin and Tonic", "type": "string" }, "price": { "description": "The price of the drink", "example": 5.99, "format": "double", "type": "number" }, "productCode": { "description": "Unique drink identifier for server requests", "example": "2438ac3c-37eb-4902-adef-ed16b4431030", "format": "uuid", "pattern": "^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$", "type": "string" }, "stock": { "description": "The stock of the drink", "example": 10, "format": "int32", "type": "integer" }, "type": { "$ref": "#/components/schemas/DrinkDrinkType" } }, "required": ["name", "type"], "title": "A drink", "type": "object" }, "speakeasyGetDrinkResponse": { "properties": { "drink": { "$ref": "#/components/schemas/speakeasyDrink" } }, "type": "object" }, "speakeasyListDrinksResponse": { "properties": { "drinks": { "items": { "$ref": "#/components/schemas/speakeasyDrink" }, "type": "array" } }, "type": "object" } } }, "externalDocs": { "description": "Speakeasy API documentation", "url": "https://github.com/speakeasy-api/speakeasy-grpc-gateway-example" }, "info": { "description": "Speakeasy API description", "title": "Speakeasy API", "version": "1.0" }, "openapi": "3.0.3", "paths": { "/v1/drinks": { "get": { "description": "Returns a list of all drinks available at the speakeasy", "operationId": "listAllDrinks", "parameters": [ { "in": "query", "name": "empty", "schema": { "type": "object" } } ], "responses": { "200": { "content": { "application/json": { "schema": { "items": { "$ref": "#/components/schemas/speakeasyDrink" }, "type": "array" } } }, "description": "" }, "default": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/rpcStatus" } } }, "description": "An unexpected error response." } }, "summary": "Get a list of drinks", "tags": ["drinks"] } }, "/v1/drinks/{productCode}": { "get": { "operationId": "getDrink", "parameters": [ { "description": "Unique drink identifier", "in": "path", "name": "productCode", "required": true, "schema": { "format": "uuid", "type": "string" } } ], "responses": { "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/speakeasyDrink" } } }, "description": "" }, "default": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/rpcStatus" } } }, "description": "An unexpected error response." } }, "summary": "Get a drink", "tags": ["drinks"], "x-speakeasy-retries": { "backoff": { "exponent": 1.5, "initialInterval": 500, "maxElapsedTime": 3600000, "maxInterval": 60000 }, "retryConnectionErrors": true, "statusCodes": ["5XX"], "strategy": "backoff" } } } }, "servers": [ { "url": "https://127.0.0.1:8080/" } ], "tags": [ { "description": "Drinks API", "name": "drinks" }, { "name": "SpeakeasyService" } ]}
Meet the Speakeasy Bar Protobuf Service
We'll start by taking a look at the Speakeasy Bar protobuf service definition in proto/speakeasy/v1/speakeasy.proto
.
The service defines one object type, called Drink
.
A service called SpeakeasyService
has two methods, GetDrink
and ListDrinks
.
Add API Information to the Service
We'll add information about the API to the service definition using options.openapiv2_swagger
from grpc.gateway.protoc_gen_openapiv2
.
We'll add a title, description, and version to the API.
This appears in the info
object in the generated OpenAPI schema.
We'll add a server to the API using the host
key.
Our conversion script will add the servers
object to the generated OpenAPI schema.
Add Descriptions and Examples to Components
To create an SDK that offers a great developer experience, we recommend adding descriptions and examples to all fields in OpenAPI components.
We'll start with the Drink
object type.
We added a title
, description
, and example
to the Drink
object type.
Note that the example
is a stringified JSON object.
We use openapiv2_field
to add options to the fields in the Drink
object type.
For example, we added a description
, pattern
, format, and example
to the productCode
field.
If you use Speakeasy to create an SDK, this description and example will appear in the generated documentation and usage examples.
This usage example is from the TypeScript SDK's documentation.
Note how the productCode
field is represented by our UUID example instead of a random string.
Customize the OperationId
By default, the operationId
is the method name in the protobuf service definition.
We can customize the operationId
for each method using options.openapiv2_operation
.
Add Descriptions and Tags to Methods
We can add descriptions and tags to methods using options.openapiv2_operation
.
Add Tag Descriptions
We can add descriptions to tags in the protobuf definition by using options.openapiv2_swagger
.
In the code example, we added a description to the drinks
tag.
Add OpenAPI Extensions
gRPC Gateway allows us to add OpenAPI extensions to the OpenAPI schema using the extensions
key in our protobuf service definition.
For example, we can add the Speakeasy retries extension x-speakeasy-retries
, which will cause the SDK to retry failed requests.
In the code example, we added the x-speakeasy-retries
extension to the GetDrink
method.
syntax = "proto3";package speakeasy;import "google/protobuf/empty.proto";import "google/api/field_behavior.proto";import "google/api/annotations.proto";import "protoc-gen-openapiv2/options/annotations.proto";option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { info: { version: "1.0", title: "Speakeasy API", description: "Speakeasy API description" }; host: "127.0.0.1:8080"; external_docs: { url: "https://github.com/speakeasy-api/speakeasy-grpc-gateway-example"; description: "Speakeasy API documentation"; }; schemes: HTTPS; tags: { name: "drinks"; description: "Drinks API"; };};message Drink { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { json_schema: { title: "A drink", description: "A drink served at the speakeasy" } example: "{\"name\": \"Gin and Tonic\",\"type\": \"DRINK_TYPE_COCKTAIL\",\"price\": 5.99,\"stock\": 10,\"productCode\": \"2438ac3c-37eb-4902-adef-ed16b4431030\"}"; }; string name = 1 [ (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The name of the drink", example: "\"Gin and Tonic\"" } ]; enum DrinkType { DRINK_TYPE_UNSPECIFIED = 0; DRINK_TYPE_WINE = 1; DRINK_TYPE_COCKTAIL = 2; DRINK_TYPE_MOCKTAIL = 3; DRINK_TYPE_SOFT = 4; DRINK_TYPE_SPIRIT = 5; DRINK_TYPE_OTHER = 6; DRINK_TYPE_BEER = 7; }; DrinkType type = 2 [ (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The type of drink", example: "\"DRINK_TYPE_COCKTAIL\"" } ]; double price = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The price of the drink", minimum: 0, example: "5.99" } ]; int32 stock = 4 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The stock of the drink", minimum: 0, example: "10" } ]; string productCode = 5 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$", description: "Unique drink identifier for server requests", format: "uuid", example: "\"2438ac3c-37eb-4902-adef-ed16b4431030\"" }];};message ListDrinksResponse { repeated Drink drinks = 1; }message ListDrinksRequest { google.protobuf.Empty empty = 1; }message GetDrinkResponse { Drink drink = 1; }message GetDrinkRequest { string product_code = 1 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$", description: "Unique drink identifier", format: "uuid", example: "\"2438ac3c-37eb-4902-adef-ed16b4431030\"" }]; }service SpeakeasyService { // List all drinks rpc ListDrinks(ListDrinksRequest) returns (ListDrinksResponse) { option (google.api.http) = { get: "/v1/drinks" response_body: "drinks" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Get a list of drinks"; description: "Returns a list of all drinks available at the speakeasy"; operation_id: "listAllDrinks"; tags: "drinks"; }; }; // Get a drink by product code rpc GetDrink(GetDrinkRequest) returns (GetDrinkResponse) { option (google.api.http) = { get: "/v1/drinks/{product_code}" response_body: "drink" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Get a drink"; operation_id: "getDrink"; tags: "drinks"; extensions: { key: "x-speakeasy-retries"; value: { struct_value: { fields { key: "strategy"; value { string_value: "backoff"; } } fields { key: "backoff"; value { struct_value: { fields { key: "initialInterval"; value { number_value: 500; } } fields { key: "maxInterval"; value { number_value: 60000; } } fields { key: "maxElapsedTime"; value { number_value: 3600000; } } fields { key: "exponent"; value { number_value: 1.5; } } } } } fields { key: "statusCodes"; value { list_value: { values { string_value: "5XX"; } } } } fields { key: "retryConnectionErrors"; value { bool_value: true; } } } } } }; };};
{ "components": { "schemas": { "DrinkDrinkType": { "default": "DRINK_TYPE_UNSPECIFIED", "enum": [ "DRINK_TYPE_UNSPECIFIED", "DRINK_TYPE_WINE", "DRINK_TYPE_COCKTAIL", "DRINK_TYPE_MOCKTAIL", "DRINK_TYPE_SOFT", "DRINK_TYPE_SPIRIT", "DRINK_TYPE_OTHER", "DRINK_TYPE_BEER" ], "type": "string" }, "protobufAny": { "additionalProperties": {}, "properties": { "@type": { "type": "string" } }, "type": "object" }, "rpcStatus": { "properties": { "code": { "format": "int32", "type": "integer" }, "details": { "items": { "$ref": "#/components/schemas/protobufAny" }, "type": "array" }, "message": { "type": "string" } }, "type": "object" }, "speakeasyDrink": { "description": "A drink served at the speakeasy", "example": { "name": "Gin and Tonic", "price": 5.99, "productCode": "2438ac3c-37eb-4902-adef-ed16b4431030", "stock": 10, "type": "DRINK_TYPE_COCKTAIL" }, "properties": { "name": { "description": "The name of the drink", "example": "Gin and Tonic", "type": "string" }, "price": { "description": "The price of the drink", "example": 5.99, "format": "double", "type": "number" }, "productCode": { "description": "Unique drink identifier for server requests", "example": "2438ac3c-37eb-4902-adef-ed16b4431030", "format": "uuid", "pattern": "^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$", "type": "string" }, "stock": { "description": "The stock of the drink", "example": 10, "format": "int32", "type": "integer" }, "type": { "$ref": "#/components/schemas/DrinkDrinkType" } }, "required": ["name", "type"], "title": "A drink", "type": "object" }, "speakeasyGetDrinkResponse": { "properties": { "drink": { "$ref": "#/components/schemas/speakeasyDrink" } }, "type": "object" }, "speakeasyListDrinksResponse": { "properties": { "drinks": { "items": { "$ref": "#/components/schemas/speakeasyDrink" }, "type": "array" } }, "type": "object" } } }, "externalDocs": { "description": "Speakeasy API documentation", "url": "https://github.com/speakeasy-api/speakeasy-grpc-gateway-example" }, "info": { "description": "Speakeasy API description", "title": "Speakeasy API", "version": "1.0" }, "openapi": "3.0.3", "paths": { "/v1/drinks": { "get": { "description": "Returns a list of all drinks available at the speakeasy", "operationId": "listAllDrinks", "parameters": [ { "in": "query", "name": "empty", "schema": { "type": "object" } } ], "responses": { "200": { "content": { "application/json": { "schema": { "items": { "$ref": "#/components/schemas/speakeasyDrink" }, "type": "array" } } }, "description": "" }, "default": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/rpcStatus" } } }, "description": "An unexpected error response." } }, "summary": "Get a list of drinks", "tags": ["drinks"] } }, "/v1/drinks/{productCode}": { "get": { "operationId": "getDrink", "parameters": [ { "description": "Unique drink identifier", "in": "path", "name": "productCode", "required": true, "schema": { "format": "uuid", "type": "string" } } ], "responses": { "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/speakeasyDrink" } } }, "description": "" }, "default": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/rpcStatus" } } }, "description": "An unexpected error response." } }, "summary": "Get a drink", "tags": ["drinks"], "x-speakeasy-retries": { "backoff": { "exponent": 1.5, "initialInterval": 500, "maxElapsedTime": 3600000, "maxInterval": 60000 }, "retryConnectionErrors": true, "statusCodes": ["5XX"], "strategy": "backoff" } } } }, "servers": [ { "url": "https://127.0.0.1:8080/" } ], "tags": [ { "description": "Drinks API", "name": "drinks" }, { "name": "SpeakeasyService" } ]}
syntax = "proto3";package speakeasy;import "google/protobuf/empty.proto";import "google/api/field_behavior.proto";import "google/api/annotations.proto";message Drink { string name = 1 [ (google.api.field_behavior) = REQUIRED ]; enum DrinkType { DRINK_TYPE_UNSPECIFIED = 0; DRINK_TYPE_WINE = 1; DRINK_TYPE_COCKTAIL = 2; DRINK_TYPE_MOCKTAIL = 3; DRINK_TYPE_SOFT = 4; DRINK_TYPE_SPIRIT = 5; DRINK_TYPE_OTHER = 6; DRINK_TYPE_BEER = 7; }; DrinkType type = 2 [ (google.api.field_behavior) = REQUIRED ]; double price = 3; int32 stock = 4; string productCode = 5;};message ListDrinksResponse { repeated Drink drinks = 1; }message ListDrinksRequest { google.protobuf.Empty empty = 1; }message GetDrinkResponse { Drink drink = 1; }message GetDrinkRequest { string product_code = 1; }service SpeakeasyService { // List all drinks rpc ListDrinks(ListDrinksRequest) returns (ListDrinksResponse) { option (google.api.http) = { get: "/v1/drinks" response_body: "drinks" }; }; // Get a drink by product code rpc GetDrink(GetDrinkRequest) returns (GetDrinkResponse) { option (google.api.http) = { get: "/v1/drinks/{product_code}" response_body: "drink" }; };};
Create an SDK With Speakeasy
Now that we have an OpenAPI 3.0 schema, we can create an SDK with Speakeasy. Speakeasy will create documentation and usage examples based on the descriptions and examples we added.
We'll use the speakeasy quickstart
command to create an SDK for the Speakeasy Bar gRPC service.
Run the following in the terminal:
speakeasy quickstart
Follow the onscreen prompts to provide the necessary configuration details for your new SDK such as the name, schema location and output path. Enter openapi/speakeasy/v1/speakeasy.openapi.json
when prompted for the OpenAPI document location and select TypeScript when prompted for which language you would like to generate.
Example Protobuf Definition and SDK Generator
The source code for our complete example is available in the gRPC Speakeasy Bar example repository (opens in a new tab).
The repository contains a TypeScript SDK and instructions on how to create more SDKs.
You can clone this repository to test how changes to the protobuf definition result in changes to the SDK.
After modifying your protobuf definition, you can run the following in the terminal to create a new SDK:
buf generate && go run convert/convert.go && speakeasy quickstart
Happy generating!