Comparison guide: OpenAPI/Swagger Go client generation
Speakeasy generates idiomatic Go SDKs based on OpenAPI specifications.
In this post, we’ll take a look at why many of our users choose to switch from OpenAPI Generate and other open-source generators to Speakeasy to generate their Go SDKs.
Open-source SDK generators play an important role in experimentation and smaller custom integrations but we believe that teams should publish high-quality SDKs for their APIs that offer the best developer experience. Usable SDKs drive adoption by making it easier for developers to switch to your API.
At Speakeasy, we generate idiomatic client SDKs in a variety of languages. Our generators follow principles that ensure we generate SDKs that offer the best developer experience so that you can focus on building your API, and your developer-users can focus on delighting their users.
Go SDK generator options
We’ll compare four Go SDK generators:
- The Go generator (opens in a new tab) from the OpenAPI Generator (opens in a new tab) project.
- oapi-codegen (opens in a new tab), an open-source OpenAPI Client and Server Code Generator.
- ogen (opens in a new tab), an open-source OpenAPI v3 code generator for Go.
- The Speakeasy SDK generator.
Below is the summary of how the four evaluated generators compare. Our recommendation is to use Speakeasy for generating Go SDKs for your APIs (1st SDK free). If you are committed to using an open source generator, we strongly recommend that you use oapi-codegen (opens in a new tab)
Feature | Speakeasy | OpenAPI Generator | oapi-codegen | ogen |
---|---|---|---|---|
Version Support | ✅ Go 1.14+ | ⚠️ Variable support | ✅ Good compatibility | ⚠️ Newer Go versions |
Dependencies | ✅ 3 deps | ❌ 1500+ deps, requires Java | ✅ No external deps | ✅ No external deps |
Go Idiomaticity | ✅ Native Go patterns | ❌ Non-idiomatic | ✅ Simple patterns | ⚠️ Custom patterns |
Nil-safe Getters | ✅ Built-in safety | ❌ No safety | ❌ No safety | ❌ Custom optionals |
Union Types | ✅ Full support | ❌ No support | ⚠️ Limited support | ✅ Full support |
Enums | ✅ Type-safe | ❌ Strings only | ✅ Type-safe | ✅ Type-safe |
Complex Numbers | ✅ Big decimal | ❌ Basic types | ❌ Basic types | ❌ Basic types |
Error Handling | ✅ Custom types | ⚠️ Generic errors | ⚠️ Basic errors | ✅ Good errors |
Retries | ✅ Built-in | ❌ No support | ❌ No support | ❌ No support |
Pagination | ✅ Built-in | ⚠️ Manual only | ⚠️ Manual only | ⚠️ Manual only |
Documentation | ✅ Complete docs | ⚠️ Basic docs | ❌ Minimal docs | ⚠️ Basic docs |
CI/CD Integration | ✅ GitHub Actions | ❌ Manual only | ❌ Manual only | ❌ Manual only |
If you want the detailed technical breakdown, full of code comparisons, read on!
Installing SDK generators
To start our comparison, we installed all four generators on a local machine running macOS.
Installing the OpenAPI Generator CLI
OpenAPI Generator depends on Java, which we covered at length previously. We concluded that managing the OpenAPI Generator Java dependencies manually just wasn’t worth the effort.
Installing openapi-generator
using Homebrew installs openjdk@11
and its many dependencies:
brew install openapi-generator
This adds the openapi-generator
CLI to our path.
Installing oapi-codegen
We install oapi-codegen using the Go package manager:
go install github.com/deepmap/oapi-codegen/cmd/oapi-codegen@latest
This command installs the oapi-codegen Go module and its dependencies (opens in a new tab).
Installing ogen
We followed the ogen quick start (opens in a new tab) to install ogen:
go install -v github.com/ogen-go/ogen/cmd/ogen@latest
This installs the ogen Go module with its dependencies (opens in a new tab).
How to install the Speakeasy CLI
To install the Speakeasy CLI, follow the steps in the Speakeasy Getting Started guide.
In the terminal, run:
brew install speakeasy-api/homebrew-tap/speakeasy
Next, authenticate with Speakeasy by running the following:
speakeasy auth login
This installs the Speakeasy CLI as a single binary without any dependencies.
Downloading the Swagger Petstore specification
Before we run our generators, we’ll need an OpenAPI specification to generate a Go SDK for. The standard specification for testing OpenAPI SDK generators and Swagger UI generators is the Swagger Petstore (opens in a new tab).
We’ll download the YAML specification at https://petstore3.swagger.io/api/v3/openapi.yaml (opens in a new tab) to our working directory and name it petstore.yaml
:
curl https://petstore3.swagger.io/api/v3/openapi.yaml --output petstore.yaml
Validating the Spec
Both the OpenAPI Generator and Speakeasy CLI can validate an OpenAPI spec. We’ll run both and compare the output.
Validation using OpenAPI Generator
To validate petstore.yaml
using OpenAPI Generator, run the following in the terminal:
openapi-generator validate -i petstore.yaml
The OpenAPI Generator returns two warnings:
Warnings:- Unused model: Address- Unused model: Customer[info] Spec has 2 recommendation(s).
Validation using Speakeasy
We’ll validate the spec with Speakeasy by running the following in the terminal:
speakeasy validate openapi -s petstore.yaml
The Speakeasy validator returns ten warnings, seven hints that some methods don’t specify any return values and three unused components. Each warning includes a detailed JSON-formatted error with line numbers.
Since both validators validated the spec with only warnings, we can assume that both will generate SDKs without issues.
Generating an SDK
Now that we know our OpenAPI spec is valid, we can start generating SDKs.
We’ll generate each SDK in a unique subdirectory, relative to where we saved the petstore.yaml
spec file.
OpenAPI generate
OpenAPI Generator features SDK generators for multiple languages, often with multiple options per language. We’ll test the Go generator in this post.
We’ll generate an SDK by running the following in the terminal:
# Generate Petstore SDK using go generatoropenapi-generator generate \--input-spec petstore.yaml \--generator-name go \--output ./petstore-sdk-go-openapi
This command will print a list of files generated and populate the new petstore-sdk-go-openapi
directory.
Generating an SDK using oapi-codegen
Before we generate an SDK using oapi-codegen, we’ll need to create a new directory for this SDK.
Run the following in the terminal:
mkdir petstore-sdk-go-oapi-codegen && cd petstore-sdk-go-oapi-codegen
Create a Go module in the new directory:
go mod init petstore-sdk-go-oapi-codegen
Then run the oapi-codegen Go module:
go run github.com/deepmap/oapi-codegen/cmd/oapi-codegen@latest -package petstore ../petstore.yaml > petstore.gen.go
Generating an SDK using ogen
We followed the ogen quick start documentation.
Create a new directory for our ogen SDK and navigate to it in the terminal:
mkdir petstore-sdk-go-ogen && cd petstore-sdk-go-ogen
Create a new Go module in this directory:
go mod init petstore-sdk-go-ogen
Copy the petstore.yaml
spec into this directory:
cp ../petstore.yaml .
Create a new file called generate.go
with the following contents:
package project//go:generate go run github.com/ogen-go/ogen/cmd/ogen@latest --target petstore --clean --no-server petstore.yaml
Then run the following from our new directory:
go generate ./...
In our testing, this resulted in a stack trace and returned an error status:
INFO convenient Convenient errors are not available {"reason": "operation has no \"default\" response", "at": "petstore.yaml:59:9"}generate:main.run/Users/ritza/go/pkg/mod/github.com/ogen-go/ogen@v0.69.1/cmd/ogen/main.go:304- build IR:main.generate/Users/ritza/go/pkg/mod/github.com/ogen-go/ogen@v0.69.1/cmd/ogen/main.go:64- make ir:github.com/ogen-go/ogen/gen.NewGenerator/Users/ritza/go/pkg/mod/github.com/ogen-go/ogen@v0.69.1/gen/generator.go:112- operations:github.com/ogen-go/ogen/gen.(*Generator).makeIR/Users/ritza/go/pkg/mod/github.com/ogen-go/ogen@v0.69.1/gen/generator.go:130- path "/pet": put:github.com/ogen-go/ogen/gen.(*Generator).makeOps/Users/ritza/go/pkg/mod/github.com/ogen-go/ogen@v0.69.1/gen/generator.go:171- requestBody:github.com/ogen-go/ogen/gen.(*Generator).generateOperation/Users/ritza/go/pkg/mod/github.com/ogen-go/ogen@v0.69.1/gen/gen_operation.go:49- contents:github.com/ogen-go/ogen/gen.(*Generator).generateRequest/Users/ritza/go/pkg/mod/github.com/ogen-go/ogen@v0.69.1/gen/gen_request_body.go:27- media: "application/x-www-form-urlencoded":github.com/ogen-go/ogen/gen.(*Generator).generateContents/Users/ritza/go/pkg/mod/github.com/ogen-go/ogen@v0.69.1/gen/gen_contents.go:330- form parameter "tags":github.com/ogen-go/ogen/gen.(*Generator).generateFormContent/Users/ritza/go/pkg/mod/github.com/ogen-go/ogen@v0.69.1/gen/gen_contents.go:206- nested objects not allowed:github.com/ogen-go/ogen/gen.isParamAllowed/Users/ritza/go/pkg/mod/github.com/ogen-go/ogen@v0.69.1/gen/gen_parameters.go:184exit status 1generate.go:3: running "go": exit status 1
The function isParamAllowed
in gen/gen_parameters.go
on line 184 throws the error that nested objects are not allowed in form parameters. This seems to indicate that ogen does not yet support generating an SDK for a form request that takes nested objects as parameters, such as a pet’s tags, when updating a pet in our schema.
The ogen documentation references a spec to download, so we’ll replace petstore.yaml
with their spec by running the following:
curl https://raw.githubusercontent.com/ogen-go/web/main/examples/petstore.yml --output petstore.yaml
With this new simplified spec, we’ll try the generator again:
go generate ./...
The generator runs without errors and prints a log line:
INFO convenient Convenient errors are not available {"reason": "operation has no \"default\" response", "at": "petstore.yaml:19:9"}
This log line seems to indicate that some operations don’t return a default response.
Speakeasy generate
Finally, we’ll generate an SDK using the Speakeasy CLI.
# Generate Petstore SDK using Speakeasy go generatorspeakeasy generate sdk \--schema petstore.yaml \--lang go \--out ./petstore-sdk-go-speakeasy/
This command prints a log of warnings and information, then completes successfully.
SDK code compared: Package structure
We now have four different Go SDKs for the Petstore API:
./petstore-sdk-go-openapi/
, generated by OpenAPI Generator../petstore-sdk-go-oapi-codegen/
, generated by oapi-codegen../petstore-sdk-go-ogen/
, generated by ogen../petstore-sdk-go-speakeasy/
, generated by Speakeasy.
We’ll start our comparison by looking at the structure of each generated SDK.
Let’s print a tree structure for each SDK’s directory. Run tree
in the terminal from our root directory:
tree -L 3 -F petstore-sdk-go-*
We’ll split the output by directory for each SDK below.
OpenAPI Generator SDK structure
petstore-sdk-go-openapi/├── README.md├── api/│ └── openapi.yaml├── api_pet.go├── api_store.go├── api_user.go├── client.go├── configuration.go├── docs/│ ├── Address.md│ ├── ApiResponse.md│ ├── Category.md│ ├── Customer.md│ ├── Order.md│ ├── Pet.md│ ├── PetApi.md│ ├── StoreApi.md│ ├── Tag.md│ ├── User.md│ └── UserApi.md├── git_push.sh├── go.mod├── go.sum├── model_address.go├── model_api_response.go├── model_category.go├── model_customer.go├── model_order.go├── model_pet.go├── model_tag.go├── model_user.go├── response.go├── test/│ ├── api_pet_test.go│ ├── api_store_test.go│ └── api_user_test.go└── utils.go
OpenAPI Generator creates a relatively flat directory structure, with dedicated directories only for a copy of the spec (api/openapi.yaml
), documentation (docs/
), and tests (test/
).
oapi-codegen SDK structure
petstore-sdk-go-oapi-codegen/├── go.mod└── petstore.gen.go
oapi-codegen creates only one file for all generated code, with no tests or documentation outside this file. This generator appears to be better suited to generating a small and specific client or server as part of a larger project, rather than generating a usable SDK that can be packaged for users.
ogen SDK structure
petstore-sdk-go-ogen/├── generate.go├── go.mod├── petstore/│ ├── oas_cfg_gen.go│ ├── oas_client_gen.go│ ├── oas_interfaces_gen.go│ ├── oas_json_gen.go│ ├── oas_parameters_gen.go│ ├── oas_request_encoders_gen.go│ ├── oas_response_decoders_gen.go│ ├── oas_schemas_gen.go│ └── oas_validators_gen.go└── petstore.yaml
ogen also generates relatively few files, which does not seem to be because this generation was based on a simpler spec. This generator does not seem to split schemas into different files and does not create any tests or documentation.
Speakeasy SDK structure
petstore-sdk-go-speakeasy/├── README.md*├── USAGE.md*├── docs/│ ├── models/│ │ ├── operations/│ │ └── shared/│ └── sdks/│ ├── pet/│ ├── sdk/│ ├── store/│ └── user/├── files.gen*├── gen.yaml*├── go.mod*├── go.sum*├── pet.go*├── pkg/│ ├── models/│ │ ├── operations/│ │ └── shared/│ ├── types/│ │ ├── bigint.go*│ │ ├── date.go*│ │ └── datetime.go*│ └── utils/│ ├── contenttype.go*│ ├── form.go*│ ├── headers.go*│ ├── pathparams.go*│ ├── queryparams.go*│ ├── requestbody.go*│ ├── retries.go*│ ├── security.go*│ └── utils.go*├── sdk.go*├── store.go*└── user.go*
Speakeasy generates a clear file structure, split into directories for models, types, and other utils. It also creates documentation, split by models and endpoints.
Models
Let’s compare how a pet is represented in each of the four SDKs:
OpenAPI Generator pet model
// OpenAPI Generator pet modeltype Pet struct {Id *int64 `json:"id,omitempty"`Name string `json:"name"`Category *Category `json:"category,omitempty"`PhotoUrls []string `json:"photoUrls"`Tags []Tag `json:"tags,omitempty"`// pet status in the storeStatus *string `json:"status,omitempty"`}
OpenAPI Generator does not seem to take the spec’s enum for pet status when generating the pet model. Status in this model is a pointer to a string, while other generators create a type to validate the status field. This model includes struct tags for JSON only.
oapi-codegen pet model
// oapi-codegen pet modeltype Pet struct {Category *Category `json:"category,omitempty"`Id *int64 `json:"id,omitempty"`Name string `json:"name"`PhotoUrls []string `json:"photoUrls"`Status *PetStatus `json:"status,omitempty"`Tags *[]Tag `json:"tags,omitempty"`}
The oapi-codegen pet model is similar to the OpenAPI Generator version, but it makes the Tags
field a pointer to a slice of Tag
, making it possible for this field to be nil
(which would be omitted from the JSON due to omitempty
).
The Status
field is not a simple string pointer, but a pointer to PetStatus
, which provides better type safety, since PetStatus
is a type alias for string
with specific allowable values.
ogen pet model
// ogen pet modeltype Pet struct {ID OptInt64 `json:"id"`Name string `json:"name"`PhotoUrls []string `json:"photoUrls"`Status OptPetStatus `json:"status"`}
The pet model generated by ogen lacks the Tags
and Category
fields because these fields are not present in the simplified spec used.
This struct uses a different approach to optional fields. It uses OptInt64
and OptPetStatus
types. We’ll look at how ogen differs from Speakeasy in terms of nullable fields below.
Speakeasy pet model
// Speakeasy pet modeltype Pet struct {Category *Category `json:"category,omitempty" form:"name=category,json"`ID *int64 `json:"id,omitempty" form:"name=id"`Name string `json:"name" form:"name=name"`PhotoUrls []string `json:"photoUrls" form:"name=photoUrls"`// pet status in the storeStatus *PetStatus `json:"status,omitempty" form:"name=status"`Tags []Tag `json:"tags,omitempty" form:"name=tags,json"`}
This struct is similar to the OpenAPI Generator version but includes additional form
struct tags, which are likely used to specify how these fields should be encoded and decoded when used in form data (such as in an HTTP POST request).
Like the oapi-codegen version, Status
is a *PetStatus
rather than a *string
.
Nullable fields
Let’s focus on the difference between how ogen and Speakeasy handle the nullable Status
field.
Here’s the relevant code generated by ogen:
type PetStatus stringconst (PetStatusAvailable PetStatus = "available"PetStatusPending PetStatus = "pending"PetStatusSold PetStatus = "sold")// OptPetStatus is optional PetStatus.type OptPetStatus struct {Value PetStatusSet bool}
While much safer than the OpenAPI Generator’s pointer to a string type, the ogen OptPetStatus
is not idiomatic and provides no benefit over using pointers, as Speakeasy does:
type PetStatus stringconst (PetStatusAvailable PetStatus = "available"PetStatusPending PetStatus = "pending"PetStatusSold PetStatus = "sold")func (e PetStatus) ToPointer() *PetStatus {return &e}
The Speakeasy approach provides the same strong typing as the ogen version. It defines PetStatus
as a custom string type and defines allowable values as constants. This practice ensures that you can’t accidentally set a PetStatus
to an invalid value.
The way Speakeasy handles the PetStatus
type is more idiomatic to Go, which generally favors simplicity and readability. Instead of defining a new struct like OptPetStatus
, Speakeasy uses a built-in language feature (pointers) to achieve the same effect. This approach is simpler, more consistent with the rest of the language, and easier to understand and use.
SDK dependencies
The ogen and oapi-codegen SDKs don’t add any dependencies to the generated modules, so we’ll compare dependencies between OpenAPI Generator and Speakeasy SDKs.
We’ll run the following for each of these two SDKs:
go mod graph
For Speakeasy, this command prints the following:
openapi github.com/cenkalti/backoff/v4@v4.2.0openapi github.com/spyzhov/ajson@v0.8.0
The output for the OpenAPI Generator version is too long to show here, so we’ll do a count instead:
go mod graph | wc -l#> 1538
Speakeasy purposefully generates SDKs with fewer dependencies, which leads to faster installs, reduced build times, and less exposure to potential security vulnerabilities.
To see why the Speakeasy SDK depends on an exponential backoff module, let’s discuss retries.
Retries
The SDK generated by Speakeasy can automatically retry failed network requests or retry requests based on specific error responses, providing a straightforward developer experience for error handling.
To enable this feature, we need to use the Speakeasy x-speakeasy-retries
extension to the OpenAPI spec. We’ll update the OpenAPI spec to add retries to the addPet
operation as a test.
Edit petstore.yaml
and add the following to the addPet
operation:
x-speakeasy-retries:strategy: backoffbackoff:initialInterval: 500 # 500 millisecondsmaxInterval: 60000 # 60 secondsmaxElapsedTime: 3600000 # 5 minutesexponent: 1.5
Add this snippet to the operation:
#...paths:/pet:# ...post:#...operationId: addPetx-speakeasy-retries:strategy: backoffbackoff:initialInterval: 500 # 500 millisecondsmaxInterval: 60000 # 60 secondsmaxElapsedTime: 3600000 # 5 minutesexponent: 1.5
Now we’ll rerun the Speakeasy generator to enable retries for failed network requests when creating a new pet. It is also possible to enable retries for the SDK as a whole by adding global x-speakeasy-retries
at the root of the OpenAPI spec.
Generated documentation
Both Speakeasy and OpenAPI generate documentation for the generated code.
Each generator creates a README.md
file at the base directory of the generated SDK. This file serves as a primary source of documentation for the SDK users. You have the option to customize this README using Speakeasy to suit your needs better. For example, you could add your brand’s logo, provide links for support, outline a code of conduct, or include any other information that could be useful to the developers using the SDK.
The Speakeasy-generated documentation really shines when it comes to usage examples, which include working usage examples for all operations, complete with imports and appropriately formatted string examples. For instance, if a string is formatted as email
in our OpenAPI spec, Speakeasy generates usage examples with strings that look like email addresses. Types formatted as uri
will generate examples that look like URLs. This makes example code clear and scannable.
We’ll test this by adding format: uri
to the items in the photoUrls
array. Let’s compare the generated example code for the addPet
endpoint after adding this string format.
Usage example generated by OpenAPI
Here’s the example from the OpenAPI-generated documentation:
package mainimport ("context""fmt""os"openapiclient "github.com/GIT_USER_ID/GIT_REPO_ID")func main() {pet := *openapiclient.NewPet("doggie", []string{"PhotoUrls_example"}) // Pet | Create a new pet in the storeconfiguration := openapiclient.NewConfiguration()apiClient := openapiclient.NewAPIClient(configuration)resp, r, err := apiClient.PetApi.AddPet(context.Background()).Pet(pet).Execute()if err != nil {fmt.Fprintf(os.Stderr, "Error when calling `PetApi.AddPet``: %v\n", err)fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r)}// response from `AddPet`: Petfmt.Fprintf(os.Stdout, "Response from `PetApi.AddPet`: %v\n", resp)}
Note how the OpenAPI example only includes required fields and ignores the URI string format from our spec.
Usage example generated by Speakeasy
This is what Speakeasy generates as a usage example:
package mainimport("context""log""openapi""openapi/pkg/models/shared""openapi/pkg/models/operations")func main() {s := sdk.New()ctx := context.Background()res, err := s.Pet.AddPetJSON(ctx, shared.Pet{Category: &shared.Category{ID: sdk.Int64(1),Name: sdk.String("Dogs"),},ID: sdk.Int64(10),Name: "doggie",PhotoUrls: []string{"https://ecstatic-original.info",},Status: shared.PetStatusSold.ToPointer(),Tags: []shared.Tag{shared.Tag{ID: sdk.Int64(681820),Name: sdk.String("Stacy Moore"),},shared.Tag{ID: sdk.Int64(697631),Name: sdk.String("Brenda Wisozk"),},shared.Tag{ID: sdk.Int64(670638),Name: sdk.String("Connie Herzog"),},shared.Tag{ID: sdk.Int64(315428),Name: sdk.String("Corey Hane III"),},},}, operations.AddPetJSONSecurity{PetstoreAuth: "",})if err != nil {log.Fatal(err)}if res.Pet != nil {// handle response}}
The example generated by Speakeasy includes all available fields and correctly formats the example string in the PhotoUrls
field.
We’ll also compare how OpenAPI and Speakeasy generate documentation for the Status
field in our pet model.
OpenAPI generate does not document enums
The OpenAPI-generated documentation reflects the generated code’s omission of valid options for the Status
field. Here’s the pet model documentation generated by OpenAPI:
Pet properties generated by OpenAPI
Name | Type | Description | Notes |
---|---|---|---|
Id | Pointer to int64 | [optional] | |
Name | string | ||
Category | Pointer to Category | [optional] | |
PhotoUrls | []string | ||
Tags | Pointer to []Tag | [optional] | |
Status | Pointer to string | pet status in the store | [optional] |
Note how Status
is simply a string, with no indication of possible values.
Speakeasy generates documentation showing valid values
Here’s how Speakeasy documents the pet model:
Pet fields generated by Speakeasy
Field | Type | Required | Description | Example |
---|---|---|---|---|
Category | *Category | :heavy_minus_sign: | N/A | |
ID | *int64 | :heavy_minus_sign: | N/A | 10 |
Name | string | :heavy_check_mark: | N/A | doggie |
PhotoUrls | []string | :heavy_check_mark: | N/A | |
Status | *PetStatus | :heavy_minus_sign: | pet status in the store | |
Tags | []Tag | :heavy_minus_sign: | N/A |
In the example above, PetStatus
links to the following documentation:
PetStatus values generated by Speakeasy
Name | Value |
---|---|
PetStatusAvailable | available |
PetStatusPending | pending |
PetStatusSold | sold |
This further illustrates Speakeasy’s attention to detail when it comes to documentation.
Automation
This comparison focuses on the installation and usage of command line generators, but the Speakeasy generator can also run as part of a CI workflow, for instance as a GitHub Action (opens in a new tab), to make sure your SDK is always up to date when your API spec changes.
Summary
We’ve seen how Speakeasy generates lightweight, idiomatic SDKs for Go.
If you’re building an API that developers rely on and would like to publish full-featured Go SDKs that follow best practices, we strongly recommend giving the Speakeasy SDK generator a try.
Join our Slack community (opens in a new tab) to let us know how we can improve our Go SDK generator or to suggest features.