Skip to main content

ogen: OpenAPI v3 code generator for Go

· 10 min read
tdakkota
Aleksandr Razumov

The ogen project generates code from an OpenAPI specification, freeing you from writing hundreds (or even thousands) of lines of boring boilerplate code on Go.

It generates client and server implementations for Go. Developers just need to implement the request handler. No interface{} and no reflect, only strong types and codegen.

In this article I will tell what makes ogen different from other code generators and why you should try it.

Strong types

ogen generates strongly-typed client and server, similar to gRPC. Also, ogen adds endpoint description for each generated method.

For the server, the Handler interface is generated that needs to be implemented:

// Handler handles operations described by OpenAPI v3 specification.
type Handler interface {
// AddPet implements addPet operation.
//
// Creates a new pet in the store. Duplicates are allowed.
//
// POST /pets
AddPet(ctx context.Context, req NewPet) (AddPetRes, error)
// DeletePet implements deletePet operation.
//
// Deletes a single pet based on the ID supplied.
//
// DELETE /pets/{id}
DeletePet(ctx context.Context, params DeletePetParams) (DeletePetRes, error)
// FindPetByID implements find pet by id operation.
//
// Returns a user based on a single ID, if the user does not have access to the pet.
//
// GET /pets/{id}
FindPetByID(ctx context.Context, params FindPetByIDParams) (FindPetByIDRes, error)
// FindPets implements findPets operation.
//
// Returns all pets from the system that the user has access to
//
// GET /pets
FindPets(ctx context.Context, params FindPetsParams) (FindPetsRes, error)
// PatchPet implements patchPet operation.
//
// Patch a pet.
//
// PATCH /pets/{id}
PatchPet(ctx context.Context, req UpdatePet, params PatchPetParams) (PatchPetRes, error)
}

Client code is quite similar:

func (c *Client) AddPet(ctx context.Context, request NewPet) (res AddPetRes, err error) {}

// PatchPet invokes patchPet operation.
//
// Patch a pet.
//
// PATCH /pets/{id}
func (c *Client) PatchPet(ctx context.Context, request UpdatePet, params PatchPetParams) (res PatchPetRes, err error) {}

Validation

ogen supports maxLength, minLength, pattern (regex), minimum, maximum and other validators for strings, arrays, objects and numbers.

UpdatePet:
type: object
properties:
name:
type: string
maxLength: 25
minLength: 3
pattern: '^[a-zA-Z0-9]+$'
tag:
maxLength: 10
minLength: 1
pattern: '^[a-zA-Z0-9]+$'
nullable: true
type: string

Unknown and required fields

Furthermore, it is checked in the effective way that required fields are set, and unknown (if not allowed) are not passed:

// Validate required fields.
var failures []validate.FieldError
for i, mask := range [1]uint8{
0b00000001,
} {
if result := (requiredBitSet[i] & mask) ^ mask; result != 0 {
// Mask only required fields and check equality to mask using XOR.
//
// If XOR result is not zero, result is not equal to expected, so some fields are missed.
// Bits of fields which would be set are actually bits of missed fields.
missed := bits.OnesCount8(result)
for bitN := 0; bitN < missed; bitN++ {
bitIdx := bits.TrailingZeros8(result)
fieldIdx := i*8 + bitIdx
var name string
if fieldIdx < len(jsonFieldsNameOfNewPet) {
name = jsonFieldsNameOfNewPet[fieldIdx]
} else {
name = strconv.Itoa(fieldIdx)
}
failures = append(failures, validate.FieldError{
Name: name,
Error: validate.ErrFieldRequired,
})
// Reset bit.
result &^= 1 << bitIdx
}
}
}

Enum

The enum validator is fully supported. The enum values are generated as constants, and validation code is generated as well for the client and server:

// Ref: #/components/schemas/Kind
type Kind string

const (
KindCat Kind = "Cat"
KindDog Kind = "Dog"
KindFish Kind = "Fish"
KindBird Kind = "Bird"
KindOther Kind = "Other"
)

func (s Kind) Validate() error {
switch s {
case "Cat":
return nil
case "Dog":
return nil
case "Fish":
return nil
case "Bird":
return nil
case "Other":
return nil
default:
return errors.Errorf("invalid value: %v", s)
}
}

// Decode decodes Kind from json.
func (s *Kind) Decode(d *jx.Decoder) error {
if s == nil {
return errors.New("invalid: unable to decode Kind to nil")
}
v, err := d.StrBytes()
if err != nil {
return err
}
// Try to use constant string.
switch Kind(v) {
case KindCat:
*s = KindCat
case KindDog:
*s = KindDog
case KindFish:
*s = KindFish
case KindBird:
*s = KindBird
case KindOther:
*s = KindOther
default:
*s = Kind(v)
}

return nil
}


Unlike ogen, deepmap/oapi-codegen does not check enum values. It only generates the named type and constants.

Without pointers

If it is possible.

In most cases, to represent optional (or nullable) fields in Go, pointers are usually used:

type Pet struct {
// Name of the pet
Name string `json:"name"`

// Type of the pet
Tag *string `json:"tag,omitempty"`
}

This is a common, but hacky way:

  • It is easy to get null pointer exception, hello The Billion Dollar Mistake
  • It increases the GC pressure, especially if there are many objects or they are nested (e.g. []Pet)
  • It is impossible to express nullable optional when three states can be passed: empty, null and filled value. It is especially useful for PATCH operations.

ogen solves this problem by generating generic types:

// Ref: #/components/schemas/NewPet
type NewPet struct {
Name string `json:"name"`
Tag OptString `json:"tag"`
}

// OptString is optional string.
type OptString struct {
Value string
Set bool
}

It seems that deepmap/oapi-codegen cannot handle optional nullable properly:

// UpdatePet defines model for UpdatePet.
type UpdatePet struct {
Name *string `json:"name,omitempty"`
Tag *string `json:"tag"`
}

Whereas ogen generates a special OptNilString type:

// Ref: #/components/schemas/UpdatePet
type UpdatePet struct {
Name OptString `json:"name"`
Tag OptNilString `json:"tag"`
}

// OptNilString is optional nullable string.
type OptNilString struct {
Value string
Set bool
Null bool
}

Using OptNilString you can express any state: the absence of the value, null value, an empty string, and just a string.

Arrays

To represent arrays, a special type may not be generated, changing the semantics of the nil value of the slice depending on the schema. For example, if the field is optional, then nil will mean the absence of the value. If nullable, then null. For optional nullable fields, you will have to generate a wrapper.

JSON implementation without reflection

ogen does not use the standard encoding/json with its limitations in speed and capabilities. Instead, it generates a static code for encoding and decoding JSON.

// Encode encodes string as json.
func (o OptNilString) Encode(e *jx.Encoder) {
if !o.Set {
return
}
if o.Null {
e.Null()
return
}
e.Str(string(o.Value))
}

It lets you make working with json more efficient and flexible. For example, decoding a field in several passes to support oneOf with a discriminator (first the value of the discriminator field is parsed, and then the value as a whole) and without it (first all fields are traversed and the type is selected by unique fields).

Instead of encoding/json, ogen uses go-faster/jx, a heavily modified and optimized fork of jsoniter (can parse almost a gigabyte of json logs in a second per core).

Custom router

ogen uses its own, efficient statically generated router based on radix tree:

// ...
// Static code generated router with unwrapped path search.
switch {
default:
if len(elem) == 0 {
break
}
switch elem[0] {
case '/': // Prefix: "/pets"
if l := len("/pets"); len(elem) >= l && elem[0:l] == "/pets" {
elem = elem[l:]
} else {
break
}

if len(elem) == 0 {
switch r.Method {
case "GET":
s.handleFindPetsRequest([0]string{}, w, r)
case "POST":
s.handleAddPetRequest([0]string{}, w, r)
default:
s.notAllowed(w, r, "GET,POST")
}

return
}
switch elem[0] {
case '/': // Prefix: "/"
if l := len("/"); len(elem) >= l && elem[0:l] == "/" {
elem = elem[l:]
} else {
break
}
// ...

Static router allows the compiler to make many optimizations: remove unnecessary bounds checks, optimize string comparison, use optimal case search algorithm instead of binary search.

It makes ogen routing much faster than chi and echo (benchmark):

name                        time/op
Router/GithubStatic/ogen-4 18.7ns ± 3%
Router/GithubStatic/chi-4 146ns ± 2%
Router/GithubStatic/echo-4 73.7ns ± 9%
Router/GithubParam/ogen-4 34.0ns ± 3%
Router/GithubParam/chi-4 251ns ± 3%
Router/GithubParam/echo-4 118ns ± 2%
Router/GithubAll/ogen-4 56.6µs ± 3%
Router/GithubAll/chi-4 323µs ± 3%
Router/GithubAll/echo-4 173µs ± 4%

name alloc/op
Router/GithubStatic/ogen-4 0.00B
Router/GithubStatic/chi-4 0.00B
Router/GithubStatic/echo-4 0.00B
Router/GithubParam/ogen-4 0.00B
Router/GithubParam/chi-4 0.00B
Router/GithubParam/echo-4 0.00B
Router/GithubAll/ogen-4 0.00B
Router/GithubAll/chi-4 0.00B
Router/GithubAll/echo-4 0.00B

OneOf

Let's imagine that we have a schema like this:

Dog:
type: object
required:
- kind
properties:
kind:
$ref: '#/components/schemas/Kind'
bark:
type: string
Cat:
type: object
required:
- kind
properties:
kind:
$ref: '#/components/schemas/Kind'
meow:
type: string
SomePet:
type: object
discriminator:
propertyName: kind
oneOf:
- $ref: '#/components/schemas/Dog'
- $ref: '#/components/schemas/Cat'

ogen will generate code like this:

// Ref: #/components/schemas/Cat
type Cat struct {
Kind Kind `json:"kind"`
Meow OptString `json:"meow"`
}

// Ref: #/components/schemas/Dog
type Dog struct {
Kind Kind `json:"kind"`
Bark OptString `json:"bark"`
}

// Ref: #/components/schemas/SomePet
// SomePet represents sum type.
type SomePet struct {
Type SomePetType // switch on this field
Dog Dog
Cat Cat
}

As you can see, the oneOf case is chosen during the decoding process.

// func (s *SomePet) Decode(d *jx.Decoder) error
if err := d.Capture(func(d *jx.Decoder) error {
return d.ObjBytes(func(d *jx.Decoder, key []byte) error {
if found {
return d.Skip()
}
switch string(key) {
case "kind":
typ, err := d.Str()
if err != nil {
return err
}
switch typ {
case "Cat":
s.Type = CatSomePet
found = true
case "Dog":
s.Type = DogSomePet
found = true
default:
return errors.Errorf("unknown type %s", typ)
}
return nil
}
return d.Skip()
})
}); err != nil {
return errors.Wrap(err, "capture")
}
if !found {
return errors.New("unable to detect sum type variant")
}
switch s.Type {
case DogSomePet:
if err := s.Dog.Decode(d); err != nil {
return err
}
case CatSomePet:
if err := s.Cat.Decode(d); err != nil {
return err
}
default:
return errors.Errorf("inferred invalid type: %s", s.Type)
}

Whereas deepmap/oapi-codegen requires an additional manual call (Also, notice that at the moment of posting this article, the generated code is broken):

// SomePet defines model for SomePet.
type SomePet struct {
union json.RawMessage
}

func (t SomePet) Discriminator() (string, error) {
var discriminator struct {
Discriminator string `json:"kind"`
}
err := json.Unmarshal(t.union, &discriminator)
return discriminator.Discriminator, err
}

// AsCat returns the union data inside the SomePet as a Cat
func (t SomePet) AsCat() (Cat, error) {
var body Cat
err := json.Unmarshal(t.union, &body)
return body, err
}

It seems that the developer should write the entire switch-by-discriminator logic by himself.

Without discriminator

Further, ogen can work without the explicit discriminator field at all, choosing the type by unique fields:

var found bool
if err := d.Capture(func(d *jx.Decoder) error {
return d.ObjBytes(func(d *jx.Decoder, key []byte) error {
switch string(key) {
case "bark":
match := DogSomePet
if found && s.Type != match {
s.Type = ""
return errors.Errorf("multiple oneOf matches: (%v, %v)", s.Type, match)
}
found = true
s.Type = match
case "meow":
match := CatSomePet
if found && s.Type != match {
s.Type = ""
return errors.Errorf("multiple oneOf matches: (%v, %v)", s.Type, match)
}
found = true
s.Type = match
}
return d.Skip()
})
}); err != nil {
return errors.Wrap(err, "capture")
}

If there is a meow field, then the type is Cat, if there is a bark field - Dog, and if we didn't find anything, then we will get an error unable to detect sum type variant.

Detailed error messages

ogen provides detailed error messages with context, like this:

$ go generate
- petstore-expanded.yaml:218:17 -> resolve: can't find value for "components/schemas/Do1"
217 | oneOf:
→ 218 | - $ref: '#/components/schemas/Do1'
| ↑
219 | - $ref: '#/components/schemas/Cat'
220 |
221 | UpdatePet:

Summary

Main advantages of ogen:

  • Strict client and server typing
  • Validation
  • oneOf and anyOf support
  • nullable optional support
  • Built-in fast static router
  • Fast JSON handling
  • Detailed error messages