Generating OpenAPI schemas from Go types

Reflect Go types straight into OpenAPI schema models.

The golang generator runs in both directions. Generating Code turns OpenAPI schemas into Go types. This page covers the return trip: you hand it Go types, and it reflects them into libopenapi schema models you can render to YAML or JSON, or drop straight into a document you’re building.

This is the path for code-first API design. Your Go types are the source of truth, and the schema is generated from them.

The generator is part of the core libopenapi module, so there’s no separate dependency to add. Import github.com/pb33f/libopenapi/generator/golang and you’re ready to go.

Available since libopenapi vX.Y.Z.

A schema from a single type

SchemaFromType takes a reflect.Type and returns a *base.SchemaProxy. Call Render() on it to get YAML:

package main

import (
    "fmt"
    "reflect"

    "github.com/pb33f/libopenapi/generator/golang"
)

// Product is a plain Go struct.
type Product struct {
    ID    string  `json:"id"`
    Name  string  `json:"name"`
    Price float64 `json:"price"`
}

func main() {

    // reflect the Go type into an OpenAPI schema
    proxy, err := golang.SchemaFromType(reflect.TypeOf(Product{}))
    if err != nil {
        panic(err)
    }

    // render the schema to YAML
    out, err := proxy.Render()
    if err != nil {
        panic(err)
    }
    fmt.Print(string(out))
}

This prints:

type: object properties: id: type: string name: type: string price: type: number format: double required: - id - name - price

The field names come from the json tags. Every field is required, because none of them are marked omitempty. A float64 becomes number with format: double. If you have a value rather than a type, SchemaFromValue does the same thing from reflect.TypeOf(value).

A component graph from many types

SchemasFromTypes walks one or more types and returns a *SchemaSet. Named struct types become reusable components, and a field of a named type becomes a $ref to it:

package main

import (
    "fmt"
    "reflect"

    "github.com/pb33f/libopenapi/generator/golang"
)

type Category struct {
    Name   string `json:"name"`
    Parent string `json:"parent,omitempty"`
}

type Product struct {
    ID       string   `json:"id"`
    Name     string   `json:"name"`
    Category Category `json:"category"`
}

func main() {

    // reflect both types into a component graph
    set, err := golang.SchemasFromTypes(
        reflect.TypeOf(Product{}),
        reflect.TypeOf(Category{}),
    )
    if err != nil {
        panic(err)
    }

    // Components holds every named schema discovered while walking the types
    for name, proxy := range set.Components.FromOldest() {
        out, _ := proxy.Render()
        fmt.Printf("%s:\n%s\n", name, string(out))
    }
}

This prints:

Category: type: object properties: name: type: string parent: type: string required: - name Product: type: object properties: id: type: string name: type: string category: $ref: '#/components/schemas/Category' required: - category - id - name

Product.category is rendered as a $ref to the Category component rather than being inlined. The SchemaSet gives you everything you need to assemble a document:

Field Description
Root The first requested root schema, for single-root callers.
Roots Every requested root schema, keyed by generated type name.
Components Every reusable schema discovered while walking the type graph.
Diagnostics Notable decisions made while reflecting.

Adding metadata with openapi tags

Go reflection only sees the Go type. It can’t see a format, a numeric bound, or an enum, because those don’t exist in Go. The openapi struct tag fills that gap. Each tag is a ;-separated list of key=value pairs:

package main

import (
    "fmt"
    "reflect"

    "github.com/pb33f/libopenapi/generator/golang"
)

// Product carries openapi struct tags that add metadata Go types can't express.
type Product struct {
    ID    string  `json:"id" openapi:"format=uuid"`
    Price float64 `json:"price" openapi:"minimum=0"`
    Tier  string  `json:"tier" openapi:"enum=gold|silver|bronze"`
}

func main() {
    proxy, err := golang.SchemaFromType(reflect.TypeOf(Product{}))
    if err != nil {
        panic(err)
    }
    out, err := proxy.Render()
    if err != nil {
        panic(err)
    }
    fmt.Print(string(out))
}

This prints:

type: object properties: id: type: string format: uuid price: type: number minimum: 0 tier: type: string enum: - gold - silver - bronze required: - id - price - tier

The tag understands the common scalar metadata keywords:

Tag key Schema keyword
format format
title, description title, description
minimum, maximum minimum, maximum
exclusiveMinimum, exclusiveMaximum exclusiveMinimum, exclusiveMaximum
multipleOf multipleOf
minLength, maxLength, pattern minLength, maxLength, pattern
minItems, maxItems, uniqueItems minItems, maxItems, uniqueItems
minProperties, maxProperties minProperties, maxProperties
enum=a|b|c enum (values separated by |)
const const
nullable native nullability
readOnly, writeOnly, deprecated readOnly, writeOnly, deprecated

Exact schemas with provider methods

When a tag isn’t enough, a type can supply its own schema by implementing one of three interfaces. The generator checks for these before it reflects the type:

Interface Method Returns
SchemaProvider OpenAPISchema() *base.SchemaProxy a libopenapi schema proxy, built directly
SchemaYAMLProvider OpenAPISchemaYAML() string the schema as a YAML string
SchemaMetadataProvider OpenAPISchemaMetadata() any typed metadata (used by the sidecar)

SchemaYAMLProvider is the simplest. Here a Money type that Go would otherwise render as a struct declares itself a formatted string instead:

package main

import (
    "fmt"
    "reflect"

    "github.com/pb33f/libopenapi/generator/golang"
)

// Money provides its own exact OpenAPI schema as YAML.
type Money struct {
    Amount   int64
    Currency string
}

func (Money) OpenAPISchemaYAML() string {
    return "type: string\npattern: '^[0-9]+ [A-Z]{3}$'\nexample: 100 USD"
}

func main() {
    proxy, err := golang.SchemaFromType(reflect.TypeOf(Money{}))
    if err != nil {
        panic(err)
    }
    out, err := proxy.Render()
    if err != nil {
        panic(err)
    }
    fmt.Print(string(out))
}

This prints:

type: string pattern: '^[0-9]+ [A-Z]{3}$' example: 100 USD

If you can’t add methods to a type, the WithTypeSchema, WithFieldSchema, and WithFieldSchemaByJSONName options do the same thing from the outside, mapping a specific type or field to an exact schema while the rest of the model is reflected normally.

Interface unions

A Go interface with several concrete implementations maps onto a oneOf. Register the variants with WithOneOfTypes, and optionally describe the discriminator with WithDiscriminatorMapping:

package main

import (
    "fmt"
    "reflect"

    "github.com/pb33f/libopenapi/generator/golang"
)

// Animal is a Go interface with two concrete implementations.
type Animal interface {
    isAnimal()
}

type Cat struct {
    Kind string `json:"kind"`
    Name string `json:"name"`
}

type Dog struct {
    Kind string `json:"kind"`
    Name string `json:"name"`
}

func (Cat) isAnimal() {}
func (Dog) isAnimal() {}

func main() {

    // register the concrete variants for the interface, then reflect it
    set, err := golang.SchemasFromTypesWithOptions(
        []reflect.Type{reflect.TypeOf((*Animal)(nil)).Elem()},
        golang.WithOneOfTypes((*Animal)(nil), Cat{}, Dog{}),
        golang.WithDiscriminatorMapping((*Animal)(nil), "kind", map[string]string{
            "cat": "#/components/schemas/Cat",
            "dog": "#/components/schemas/Dog",
        }),
    )
    if err != nil {
        panic(err)
    }
    for name, proxy := range set.Components.FromOldest() {
        out, _ := proxy.Render()
        fmt.Printf("%s:\n%s\n", name, string(out))
    }
}

This prints:

Animal: oneOf: - $ref: '#/components/schemas/Cat' - $ref: '#/components/schemas/Dog' discriminator: propertyName: kind mapping: cat: '#/components/schemas/Cat' dog: '#/components/schemas/Dog' Cat: type: object properties: kind: type: string name: type: string required: - kind - name Dog: type: object properties: kind: type: string name: type: string required: - kind - name

Nullability

A pointer field is nullable. Reflected nullable values use JSON Schema 2020-12 native nullability rather than the OpenAPI 3.0 nullable: true keyword. A direct schema gets "null" added to its type array; a nullable reference is wrapped in an anyOf with a null branch:

package main

import (
    "fmt"
    "reflect"

    "github.com/pb33f/libopenapi/generator/golang"
)

type Product struct {
    Name        string  `json:"name"`
    Description *string `json:"description,omitempty"`
}

func main() {
    proxy, err := golang.SchemaFromType(reflect.TypeOf(Product{}))
    if err != nil {
        panic(err)
    }
    out, err := proxy.Render()
    if err != nil {
        panic(err)
    }
    fmt.Print(string(out))
}

This prints:

type: object properties: name: type: string description: type: - string - "null" required: - name

Lossless round trips

OpenAPI → Go → OpenAPI is lossy by default. Reflection only recovers what the Go type carries, so validation keywords, examples, and other metadata are dropped on the way back.

Two options on the generating side close that gap. WithOpenAPITags writes the compact openapi tags shown above onto the generated fields, and WithSchemaMetadataSidecar emits a separate source file that carries the full original schema for each type. With both enabled, the reflected schemas match the originals. We test exactly this round trip against the train-travel specification on every build.

Going the other way

To turn OpenAPI schemas into Go types, see Generating Code.