Idiomatic SDKs for OpenAPI

ndimares

Nolan Di Mare Sullivan

Posted on December 6, 2022

Idiomatic SDKs for OpenAPI

Our SDK Generator

Client SDKs are a bit like vodka, they’re either handcrafted and very expensive, or they’re cheap and leave you with nothing but regrets and a hangover. To get an SDK with a good developer experience, companies have historically needed to invest significant resources in handrolling their own. The only other option is using bare-bones OSS offerings. 

We’re providing a middle path; a free-to-use SDK generator that provides the foundation for a great developer experience with no investment required. We've launched with support for Go, Python, Typescript, and Java (alpha). We plan to add Ruby as well as other languages soon!

What constitutes a great developer experience is of course subjective, but we focused on:

  • Fully typed so that SDKs feel like they have been written by a human: easy to read and debug.
  • Batteries included where everything from retries to pagination of your APIs is handled as part of the SDK generation (Available in our closed beta).
  • Easy to use, our SDK generators are fault tolerant, and designed to always output usable SDKs where possible. If we can't output a usable SDK, we will provide human readable error messaging about why validation of your OpenAPI spec failed (instead of just failing silently or with obscure error messages or giving you a broken SDK)
  • Full OpenAPI Coverage, We plan to have broad coverage of the OpenAPI specs while having a deep focus on the most common ways of defining an API and ensuring you have a nice to use SDK as the output.

To illustrate the differences between our SDK generator and the open source examples, we’ve run the canonical Petshop Example through both and compared the output. But the TLDR is:

  • The Speakeasy SDK generator is installed with brew with no additional dependencies
  • No NPM. No Java. Everything is packaged in a self-contained binary
  • We support less languages for now but the ones we do are more idiomatic, so that usage feels natural per language.
  • We provide a simple API to the SDKs that is easy to mock and test.
  • The SDKs we produce have fully typed outputs including enums and more.

The generator has been battle tested on thousands of APIs and we are sharing the results in our github repo. If you want to try it out on your own, download the CLI or brew install and get started in minutes:

brew install speakeasy-api/homebrew-tap/speakeasy
speakeasy generate sdk -s openapi.yaml -o ./sdk -l go

Our Experience using OpenAPI generator

Before we get into the details of how our SDK generator compares with others, we wanted to walk through the experience with the OpenAPI generator that led to this product being created in the first place. It will probably be quite familiar to those who have used the OpenAPI tooling.

Installation

Ours was a struggle from the word go.  If you look at the openapi-generator website, it will direct you to ‘Try NPM’ to install the CLI. The key word here is ‘Try’, because there is no guarantee of success. We had multiple issues not already being setup for using NPM:

  • Had permissions issues installing globally via NPM
  • Got an error “Error: /bin/sh: 1: java: not found”  when first run. 

To resolve the issue, we had to install both NPM and Java before we could get the installation working. We had better luck with the homebrew install instruction further down the page. They worked on the first attempt at installation, downloading all the required dependencies.

Schema Validation

Validating our OpenAPI spec worked fine and was a great start to using the openapi-generator.

openapi-generator validate -i openapi.yaml           
Validating spec (openapi.yaml)
No validation issues detected.
Enter fullscreen mode Exit fullscreen mode

SDK Generation

Even though our schema had been deemed valid by the tool, we ran into issues when we began trying to generate a Go SDK from our openapi yaml. The error that we received was most unhelpful and we weren’t able to determine why our spec might have been causing issues.

openapi-generator generate -i openapi.yaml -g go -o ../speakeasy-client-sdk-go-openapi-gen
...
Exception: Property and is missing from getVars
        at org.openapitools.codegen.DefaultGenerator.processOperation(DefaultGenerator.java:1187)
        at org.openapitools.codegen.DefaultGenerator.processPaths(DefaultGenerator.java:1078)
        at org.openapitools.codegen.DefaultGenerator.generateApis(DefaultGenerator.java:580)
        at org.openapitools.codegen.DefaultGenerator.generate(DefaultGenerator.java:915)
        at org.openapitools.codegen.cmd.Generate.execute(Generate.java:465)
        at org.openapitools.codegen.cmd.OpenApiGeneratorCommand.run(OpenApiGeneratorCommand.java:32)
        at org.openapitools.codegen.OpenAPIGenerator.main(OpenAPIGenerator.java:66)
Caused by: java.lang.RuntimeException: Property and is missing from getVars
        at org.openapitools.codegen.DefaultCodegen.addRequiredVarsMap(DefaultCodegen.java:7319)
        at org.openapitools.codegen.DefaultCodegen.addVarsRequiredVarsAdditionalProps(DefaultCodegen.java:7354)
        at org.openapitools.codegen.DefaultCodegen.fromParameter(DefaultCodegen.java:4972)
        at org.openapitools.codegen.DefaultCodegen.fromOperation(DefaultCodegen.java:4394)
        at org.openapitools.codegen.DefaultGenerator.processOperation(DefaultGenerator.java:1155)
        ... 6 more
Enter fullscreen mode Exit fullscreen mode

Direct Comparison with Petshop Example

Let’s move onto an example that does work with the openapi-generator, the canonical Petstore API from the OpenAPI specification. First let’s take a look at the commands used to generate SDKs:

OpenAPI:

openapi-generator generate -i petstore.yaml -g go -o ./openapi
Enter fullscreen mode Exit fullscreen mode

Speakeasy:

speakeasy generate sdk -s petstore.yaml -o ./speakeasy -l go
Enter fullscreen mode Exit fullscreen mode

The OpenAPI and Speakeasy generators both output usable SDKs for the Petstore API. We’re using Go for this example, but our generator supports Go, Python and Typescript. Note that the openapi generator supports a large array of additional languages that we plan to add to the speakeasy generator down the road.

While the OpenAPI generator does support many languages, as we were looking through we felt that the SDKs for languages like Go were actually quite Java-like and less idiomatic to the Go language: 

OpenAPI:

ctx := context.Background()
  client := openapi.NewAPIClient(openapi.NewConfiguration())
  ctx = context.WithValue(ctx, openapi.ContextAccessToken, "special-key")
  r := client.PetApi.FindPetsByStatus(ctx)
  r = r.Status("pending")
  pets, res, err := r.Execute()
  if err != nil {
    log.Fatal(err)
  }
  if res.StatusCode != 200 {
    log.Fatalf("unexpected status code: %d", res.StatusCode)
  }
  data, _ := json.Marshal(pets)
  fmt.Println(string(data))
Enter fullscreen mode Exit fullscreen mode

Speakeasy:

ctx := context.Background()
  sdk := speakeasy.New()
  status := operations.FindPetsByStatusStatusEnumPending
  queryParams := operations.FindPetsByStatusQueryParams{Status: &status}
  security := operations.FindPetsByStatusSecurity{
    PetstoreAuth: shared.SchemePetstoreAuth{
      Authorization: "Bearer special-key",
    },
  }
  res, err := sdk.FindPetsByStatus(ctx, operations.FindPetsByStatusRequest{
    QueryParams: queryParams,
    Security:    security,
  })
  if err != nil {
    log.Fatal(err)
  }
  if res.StatusCode != 200 {
    log.Fatalf("unexpected status code: %d", res.StatusCode)
  }
  data, _ := json.Marshal(res.Pets)
  fmt.Println(string(data))
Enter fullscreen mode Exit fullscreen mode

The openapi-generator’s SDK is also harder to mock due to the multiple method calls required to set up a request and execute it.  With the Speakeasy SDK, a single method call is sufficient.

It’s also worth looking at the formatting and styling for each of the SDKs generated where there are some differences: 

  • The openapi-generator outputs comments to help with usage (coming soon for the speakeasy generator). 
  • The openapi-generator generated a lot of additional getter/setter, instantiation and serialization methods that aren’t required and just reduce the readability of the SDKs code.
  • The OpenAPI SDK code lacks formatting, whereas our SDK code is formatted idiomatically. 
  • Our generator generated full types for everything (where possible). 

That last point we feel is quite important. For example, OpenAPI treats enums as strings, whereas we generate typed enums reducing usage errors. Our support for enums comes at the cost of supporting a single edge case of the Petstore API, which allows status enums to be provided as comma separated strings to filter on multiple status, this could be overcome by defining in the OpenAPI spec that the type is an array of enums instead of just a string.

OpenAPI:

// Pet struct for Pet
  type 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 store
    Status *string `json:"status,omitempty"`
  }

  // NewPet instantiates a new Pet object
  // This constructor will assign default values to properties that have it defined,
  // and makes sure properties required by API are set, but the set of arguments
  // will change when the set of required properties is changed
  func NewPet(name string, photoUrls []string) *Pet {
    this := Pet{}
    this.Name = name
    this.PhotoUrls = photoUrls
    return &this
  }

  // NewPetWithDefaults instantiates a new Pet object
  // This constructor will only assign default values to properties that have it defined,
  // but it doesn't guarantee that properties required by API are set
  func NewPetWithDefaults() *Pet {
    this := Pet{}
    return &this
  }

  // GetId returns the Id field value if set, zero value otherwise.
  func (o *Pet) GetId() int64 {
    if o == nil || o.Id == nil {
      var ret int64
      return ret
    }
    return *o.Id
  }

  // GetIdOk returns a tuple with the Id field value if set, nil otherwise
  // and a boolean to check if the value has been set.
  func (o *Pet) GetIdOk() (*int64, bool) {
    if o == nil || o.Id == nil {
      return nil, false
    }
    return o.Id, true
  }

  // ... And continues with getters/setters for every field
Enter fullscreen mode Exit fullscreen mode

Speakeasy:

type PetStatusEnum string

  const (
    PetStatusEnumAvailable PetStatusEnum = "available"
    PetStatusEnumPending   PetStatusEnum = "pending"
    PetStatusEnumSold      PetStatusEnum = "sold"
  )

  type Pet struct {
    Category  *Category      `json:"category,omitempty"`
    ID        *int64         `json:"id,omitempty"`
    Name      string         `json:"name"`
    PhotoUrls []string       `json:"photoUrls"`
    Status    *PetStatusEnum `json:"status,omitempty"`
    Tags      []Tag          `json:"tags"`
  }
Enter fullscreen mode Exit fullscreen mode

Summary

Hope people found that comparison to be interesting/useful. We look forward to hearing feedback on how we can improve our SDK generators and what we should build next. If you have any questions, please join our slack community and you can message us directly.

💖 💪 🙅 🚩
ndimares
Nolan Di Mare Sullivan

Posted on December 6, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Idiomatic SDKs for OpenAPI
api Idiomatic SDKs for OpenAPI

December 6, 2022